diff --git a/web/src/views/login/login/index.vue b/web/src/views/login/login/index.vue
new file mode 100644
index 0000000..a6d09db
--- /dev/null
+++ b/web/src/views/login/login/index.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/login/register/agreement.vue b/web/src/views/login/register/agreement.vue
new file mode 100644
index 0000000..840c7b1
--- /dev/null
+++ b/web/src/views/login/register/agreement.vue
@@ -0,0 +1,54 @@
+
+
+ 我已阅读并接受
+
+ 《用户协议》
+
+
+ 《隐私权政策》
+
+
+
+
+
+
+
diff --git a/web/src/views/login/register/index.vue b/web/src/views/login/register/index.vue
new file mode 100644
index 0000000..f4ce11f
--- /dev/null
+++ b/web/src/views/login/register/index.vue
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ sendLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注册
+
+
+
+
+
+
+
+
+ 《{{ agreeTitle }}》
+
+
+
+
+
+
+ 我已知晓并接受
+ 我拒绝
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/netconn/columns.ts b/web/src/views/monitor/netconn/columns.ts
new file mode 100644
index 0000000..74c254c
--- /dev/null
+++ b/web/src/views/monitor/netconn/columns.ts
@@ -0,0 +1,130 @@
+import { h } from 'vue';
+import { NButton, NTag } from 'naive-ui';
+import { timestampToTime, formatBefore } from '@/utils/dateUtil';
+import { renderIcon, renderOptionTag, renderTooltip } from '@/utils';
+import { HelpCircleOutline } from '@vicons/ionicons5';
+
+export const columns = [
+ {
+ title: '连接ID',
+ key: 'id',
+ width: 80,
+ render(row) {
+ return '# ' + row.id;
+ },
+ },
+ {
+ title: '连接协议',
+ key: 'proto',
+ width: 80,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'info',
+ bordered: false,
+ },
+ {
+ default: () => row.proto,
+ }
+ );
+ },
+ },
+ {
+ title: '服务端口',
+ key: 'port',
+ width: 80,
+ render(row) {
+ return row.port;
+ },
+ },
+ {
+ title: '应用名称',
+ key: 'name',
+ width: 100,
+ render: (rows, _) => {
+ return rows.name;
+ },
+ },
+ {
+ title: '登录地址',
+ key: 'addr',
+ width: 150,
+ },
+ {
+ title(_column) {
+ return renderTooltip(
+ h(
+ NButton,
+ {
+ strong: true,
+ size: 'small',
+ text: true,
+ iconPlacement: 'right',
+ },
+ { default: () => '认证状态', icon: renderIcon(HelpCircleOutline) }
+ ),
+ '成功连接到服务端口并通过登录许可认证的客户端连接会显示为 `已认证` 状态'
+ );
+ },
+ key: 'isAuth',
+ width: 80,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.isAuth ? 'success' : 'warning',
+ bordered: false,
+ },
+ {
+ default: () => (row.isAuth ? '已认证' : '未认证'),
+ }
+ );
+ },
+ },
+ {
+ title: '授权分组',
+ key: 'group',
+ width: 100,
+ render(row) {
+ return renderOptionTag('ServerLicenseGroupOptions', row.group);
+ },
+ },
+ {
+ title: '授权许可',
+ key: 'name',
+ render(row) {
+ return h('p', { id: 'app' }, [
+ h('div', {
+ innerHTML: '
名称:' + row.licenseName + '
',
+ }),
+ h('div', {
+ innerHTML: '',
+ }),
+ ]);
+ },
+ width: 180,
+ },
+ {
+ title: '心跳',
+ key: 'heartbeatTime',
+ width: 100,
+ render: (rows, _) => {
+ return formatBefore(new Date(rows.heartbeatTime * 1000));
+ },
+ },
+ {
+ title: '登录时间',
+ key: 'firstTime',
+ width: 150,
+ render: (rows, _) => {
+ return timestampToTime(rows.firstTime);
+ },
+ },
+];
diff --git a/web/src/views/monitor/netconn/index.vue b/web/src/views/monitor/netconn/index.vue
new file mode 100644
index 0000000..0dd0130
--- /dev/null
+++ b/web/src/views/monitor/netconn/index.vue
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 许可证列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/netconn/modal/edit.vue b/web/src/views/monitor/netconn/modal/edit.vue
new file mode 100644
index 0000000..3a70330
--- /dev/null
+++ b/web/src/views/monitor/netconn/modal/edit.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+ 注意:如果服务在线,为了确保更新后的许可证信息生效,服务需要重新登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/netconn/modal/index.vue b/web/src/views/monitor/netconn/modal/index.vue
new file mode 100644
index 0000000..3fcdaf8
--- /dev/null
+++ b/web/src/views/monitor/netconn/modal/index.vue
@@ -0,0 +1,359 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加许可证
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+ 导出
+
+
+
+
+
+
+
+
+ 如果许可证未分配任何路由,则客户端可以访问所有服务路由接口
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/netconn/modal/modal.vue b/web/src/views/monitor/netconn/modal/modal.vue
new file mode 100644
index 0000000..cbdd692
--- /dev/null
+++ b/web/src/views/monitor/netconn/modal/modal.vue
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/netconn/modal/model.ts b/web/src/views/monitor/netconn/modal/model.ts
new file mode 100644
index 0000000..bab1967
--- /dev/null
+++ b/web/src/views/monitor/netconn/modal/model.ts
@@ -0,0 +1,254 @@
+import { h, ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts, formatBefore } from '@/utils/dateUtil';
+import { useDictStore } from '@/store/modules/dict';
+import { renderOptionTag } from '@/utils';
+
+const dict = useDictStore();
+
+export interface State {
+ id: number;
+ group: string;
+ name: string;
+ appid: string;
+ secretKey: string;
+ remoteAddr: string;
+ onlineLimit: number;
+ loginTimes: number;
+ lastLoginAt: string;
+ lastActiveAt: string;
+ routes: any;
+ allowedIps: string;
+ endAt: string;
+ remark: string;
+ status: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export const defaultState = {
+ id: 0,
+ group: '',
+ name: '',
+ appid: '',
+ secretKey: '',
+ remoteAddr: '',
+ onlineLimit: 1,
+ loginTimes: 0,
+ lastLoginAt: '',
+ lastActiveAt: '',
+ routes: null,
+ allowedIps: '',
+ endAt: '',
+ remark: '',
+ status: 1,
+ createdAt: '',
+ updatedAt: '',
+};
+
+export function newState(state: State | null): State {
+ if (state !== null) {
+ return cloneDeep(state);
+ }
+ return cloneDeep(defaultState);
+}
+
+export const rules = {
+ group: {
+ required: true,
+ trigger: ['blur', 'input'],
+ type: 'string',
+ message: '请输入分组',
+ },
+ name: {
+ required: true,
+ trigger: ['blur', 'input'],
+ type: 'string',
+ message: '请输入许可名称',
+ },
+ appid: {
+ required: true,
+ trigger: ['blur', 'input'],
+ type: 'string',
+ message: '请输入应用ID',
+ },
+ endAt: {
+ required: true,
+ trigger: ['blur', 'input', 'focus'],
+ type: 'string',
+ message: '请输入授权结束时间',
+ },
+};
+
+export const schemas = ref([
+ {
+ field: 'id',
+ component: 'NInput',
+ label: '许可ID',
+ componentProps: {
+ placeholder: '请输入许可ID',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'group',
+ component: 'NSelect',
+ label: '授权分组',
+ defaultValue: null,
+ componentProps: {
+ placeholder: '请选择授权分组',
+ options: dict.getOption('ServerLicenseGroupOptions'),
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'name',
+ component: 'NInput',
+ label: '许可名称',
+ componentProps: {
+ placeholder: '请输入许可名称',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'appid',
+ component: 'NInput',
+ label: 'APPID',
+ componentProps: {
+ placeholder: '请输入APPID',
+ 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);
+ },
+ },
+ },
+ {
+ field: 'endAt',
+ component: 'NDatePicker',
+ label: '过期时间',
+ componentProps: {
+ type: 'datetimerange',
+ clearable: true,
+ shortcuts: defRangeShortcuts(),
+ 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: 'group',
+ width: 100,
+ render(row) {
+ return renderOptionTag('ServerLicenseGroupOptions', row.group);
+ },
+ },
+ {
+ title: '授权许可',
+ key: 'name',
+ render(row) {
+ return h('p', { id: 'app' }, [
+ h('div', {
+ innerHTML: '',
+ }),
+ h('div', {
+ innerHTML: '',
+ }),
+ ]);
+ },
+ width: 180,
+ },
+ {
+ title: '在线',
+ key: 'online',
+ render(row) {
+ return row.online + ' / ' + row.onlineLimit;
+ },
+ width: 100,
+ },
+ {
+ title: '授权有效期',
+ key: 'endAt',
+ width: 150,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 100,
+ render(row) {
+ return renderOptionTag('sys_normal_disable', row.status);
+ },
+ },
+ {
+ title: '最后连接',
+ key: 'remoteAddr',
+ width: 150,
+ },
+ {
+ title: '最近登录 / 心跳',
+ key: 'name',
+ render(row) {
+ if (row.lastLoginAt === null) {
+ return '从未登录';
+ }
+ return (
+ formatBefore(new Date(row.lastLoginAt)) + ' / ' + formatBefore(new Date(row.lastActiveAt))
+ );
+ },
+ width: 180,
+ },
+ {
+ title: '累计登录',
+ key: 'loginTimes',
+ width: 100,
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 150,
+ },
+];
+
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable', 'ServerLicenseGroupOptions']);
+}
diff --git a/web/src/views/monitor/online/columns.ts b/web/src/views/monitor/online/columns.ts
new file mode 100644
index 0000000..8b3308e
--- /dev/null
+++ b/web/src/views/monitor/online/columns.ts
@@ -0,0 +1,122 @@
+import { h } from 'vue';
+import { NAvatar, NTag } from 'naive-ui';
+import { timestampToTime, formatBefore } from '@/utils/dateUtil';
+
+export const columns = [
+ {
+ title: '会话编号',
+ key: 'id',
+ width: 280,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'info',
+ bordered: false,
+ },
+ {
+ default: () => row.id,
+ }
+ );
+ },
+ },
+ {
+ title: '登录应用',
+ key: 'app',
+ width: 80,
+ render(row) {
+ return row.app;
+ },
+ },
+ {
+ title: '用户ID',
+ key: 'userId',
+ width: 100,
+ },
+ {
+ title: '用户名',
+ key: 'username',
+ width: 100,
+ },
+ {
+ title: '头像',
+ key: 'avatar',
+ width: 80,
+ render(row) {
+ if (row.avatar !== '') {
+ return h(NAvatar, {
+ circle: true,
+ size: 'small',
+ src: row.avatar,
+ });
+ } else {
+ return h(
+ NAvatar,
+ {
+ circle: true,
+ size: 'small',
+ },
+ {
+ default: () => row.username.substring(0, 2),
+ }
+ );
+ }
+ },
+ },
+ {
+ title: '登录IP',
+ key: 'ip',
+ width: 150,
+ },
+ // {
+ // title: 'IP地区',
+ // key: 'region',
+ // },
+ {
+ title: '浏览器',
+ key: 'browser',
+ width: 200,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'success',
+ bordered: false,
+ },
+ {
+ default: () => row.browser,
+ }
+ );
+ },
+ },
+ {
+ title: '操作系统',
+ key: 'os',
+ width: 150,
+ render(row) {
+ return row.os;
+ },
+ },
+ {
+ title: '最后活跃',
+ key: 'heartbeatTime',
+ width: 80,
+ render: (rows, _) => {
+ return formatBefore(new Date(rows.heartbeatTime * 1000));
+ },
+ },
+ {
+ title: '登录时间',
+ key: 'firstTime',
+ width: 220,
+ render: (rows, _) => {
+ return timestampToTime(rows.firstTime);
+ },
+ },
+];
diff --git a/web/src/views/monitor/online/index.vue b/web/src/views/monitor/online/index.vue
new file mode 100644
index 0000000..c210d05
--- /dev/null
+++ b/web/src/views/monitor/online/index.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/serve-log/index.vue b/web/src/views/monitor/serve-log/index.vue
new file mode 100644
index 0000000..e54880c
--- /dev/null
+++ b/web/src/views/monitor/serve-log/index.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+ 导出
+
+
+
+
+
+
+
+ {{ preview?.content }}
+
+
+
+
+
+
+
+
+ (showModal = false)">关闭
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/serve-log/model.ts b/web/src/views/monitor/serve-log/model.ts
new file mode 100644
index 0000000..18921e5
--- /dev/null
+++ b/web/src/views/monitor/serve-log/model.ts
@@ -0,0 +1,137 @@
+import { h, ref } from 'vue';
+import { NTag, NButton } from 'naive-ui';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { format } from 'date-fns';
+import { renderIcon, renderOptionTag, renderTooltip } from '@/utils';
+import { HelpCircleOutline } from '@vicons/ionicons5';
+import { useDictStore } from '@/store/modules/dict';
+
+const dict = useDictStore();
+
+export const schemas = ref([
+ {
+ field: 'traceId',
+ component: 'NInput',
+ label: '链路ID',
+ componentProps: {
+ placeholder: '请输入链路ID',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'content',
+ component: 'NInput',
+ label: '日志内容',
+ componentProps: {
+ placeholder: '请输入日志内容关键词',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'levelFormat',
+ component: 'NSelect',
+ label: '日志级别',
+ defaultValue: null,
+ componentProps: {
+ placeholder: '请选择日志级别',
+ options: dict.getOption('sys_log_type'),
+ 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: 80,
+ },
+ {
+ title(_column) {
+ return renderTooltip(
+ h(
+ NButton,
+ {
+ strong: true,
+ size: 'small',
+ text: true,
+ iconPlacement: 'right',
+ },
+ { default: () => '链路ID', icon: renderIcon(HelpCircleOutline) }
+ ),
+ 'hotgo默认支持链路追踪,如果是web请求产生的日志则还可以关联对应的访问日志'
+ );
+ },
+ key: 'traceId',
+ width: 280,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'default',
+ bordered: false,
+ checkable: true,
+ },
+ {
+ default: () => row.traceId,
+ }
+ );
+ },
+ },
+ {
+ title: '日志级别',
+ key: 'levelFormat',
+ render(row) {
+ return renderOptionTag('sys_log_type', row.levelFormat);
+ },
+ width: 120,
+ },
+ {
+ title: '日志内容',
+ key: 'content',
+ width: 320,
+ },
+ {
+ title: '触发时间',
+ key: 'triggerNs',
+ width: 200,
+ render(row) {
+ if (row.triggerNs <= 0) {
+ return '-';
+ }
+ return format(new Date(row.triggerNs / 1000000), 'yyyy-MM-dd HH:mm:ss.SSS');
+ },
+ },
+ {
+ title: '记录时间',
+ key: 'createdAt',
+ width: 150,
+ },
+];
+
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable', 'sys_log_type']);
+}
diff --git a/web/src/views/monitor/serve-monitor/components/DataItem.vue b/web/src/views/monitor/serve-monitor/components/DataItem.vue
new file mode 100644
index 0000000..cff9e6f
--- /dev/null
+++ b/web/src/views/monitor/serve-monitor/components/DataItem.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
{{ dataModel.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dataModel.data }}
+
+
+
+
+
+
+ {{ dataModel.bottomTitle }}
+ {{ dataModel.totalSum }}
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/serve-monitor/components/chart/FullYearSalesChart.vue b/web/src/views/monitor/serve-monitor/components/chart/FullYearSalesChart.vue
new file mode 100644
index 0000000..9bccef4
--- /dev/null
+++ b/web/src/views/monitor/serve-monitor/components/chart/FullYearSalesChart.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+ 实时网卡流量(全部网卡)
+
+
+
+
+
+
+
+
+
+ {{ last?.up }} KB
+
+
+ 上行
+
+
+
+
+
+ {{ last?.down ?? 0 }} KB
+
+
+ 下行
+
+
+
+
+
+ {{ last?.bytesSent ?? 0 }}
+ 总发送
+
+
+
+
+ {{ last?.bytesRecv ?? 0 }}
+ 总接收
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/monitor/serve-monitor/components/chart/LoadChart.vue b/web/src/views/monitor/serve-monitor/components/chart/LoadChart.vue
new file mode 100644
index 0000000..4652166
--- /dev/null
+++ b/web/src/views/monitor/serve-monitor/components/chart/LoadChart.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
diff --git a/web/src/views/monitor/serve-monitor/index.vue b/web/src/views/monitor/serve-monitor/index.vue
new file mode 100644
index 0000000..9252f5c
--- /dev/null
+++ b/web/src/views/monitor/serve-monitor/index.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+
+
+
+
{{ extra.data }}
+
{{ extra.data1 }}
+
+
+
+
+
已用内存:{{ extra.data }}
+
剩余内存:{{ extra.data1 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dataRunInfo.hostname }}
+
+ {{ dataRunInfo.os }}
+
+ {{ dataRunInfo.intranet_ip }} /
+ {{ dataRunInfo.public_ip }}
+
+ {{ dataRunInfo.arch }}
+
+
+
+
+ {{ dataRunInfo.version }}
+
+
+ {{ dataRunInfo.hgVersion }}
+
+ {{ dataRunInfo.startTime }}
+
+ {{ formatBefore(new Date(dataRunInfo.startTime)) }}
+
+ {{ dataRunInfo.pwd }}
+
+ {{ dataRunInfo.goroutine }}
+
+ {{ dataRunInfo.goMem }}
+ {{ dataRunInfo.goSize }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/normalTreeDemo/edit.vue b/web/src/views/normalTreeDemo/edit.vue
new file mode 100644
index 0000000..712cc71
--- /dev/null
+++ b/web/src/views/normalTreeDemo/edit.vue
@@ -0,0 +1,169 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/normalTreeDemo/index.vue b/web/src/views/normalTreeDemo/index.vue
new file mode 100644
index 0000000..c964ba4
--- /dev/null
+++ b/web/src/views/normalTreeDemo/index.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+ 批量删除
+
+
+ 全部{{ expandedKeys.length ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/normalTreeDemo/model.ts b/web/src/views/normalTreeDemo/model.ts
new file mode 100644
index 0000000..361d809
--- /dev/null
+++ b/web/src/views/normalTreeDemo/model.ts
@@ -0,0 +1,171 @@
+import { h, ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { renderOptionTag, renderPopoverMemberSumma, MemberSumma } from '@/utils';
+import { TreeOption } from '@/api/normalTreeDemo';
+import { useDictStore } from '@/store/modules/dict';
+
+const dict = useDictStore();
+
+export class State {
+ public title = ''; // 标题
+ public id = 0; // ID
+ public pid = 0; // 上级
+ public level = 1; // 关系树级别
+ public tree = ''; // 关系树
+ public categoryId = null; // 测试分类
+ public description = ''; // 描述
+ public sort = 0; // 排序
+ public status = 1; // 状态
+ public createdBy = 0; // 创建者
+ public createdBySumma?: null | MemberSumma = null; // 创建者摘要信息
+ public updatedBy = 0; // 更新者
+ public createdAt = ''; // 创建时间
+ public updatedAt = ''; // 修改时间
+ public deletedAt = ''; // 删除时间
+
+ constructor(state?: Partial) {
+ if (state) {
+ Object.assign(this, state);
+ }
+ }
+}
+
+export function newState(state: State | Record | 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: '请输入标题',
+ },
+};
+
+// 表格搜索表单
+export const schemas = ref([
+ {
+ field: 'title',
+ component: 'NInput',
+ label: '标题',
+ componentProps: {
+ placeholder: '请输入标题',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'categoryId',
+ component: 'NSelect',
+ label: '测试分类',
+ defaultValue: null,
+ componentProps: {
+ placeholder: '请选择测试分类',
+ options: dict.getOption('testCategoryOption'),
+ 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);
+ },
+ },
+ },
+ {
+ field: 'createdAt',
+ component: 'NDatePicker',
+ label: '创建时间',
+ componentProps: {
+ type: 'datetimerange',
+ clearable: true,
+ shortcuts: defRangeShortcuts(),
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+]);
+
+// 表格列
+export const columns = [
+ {
+ title: '标题',
+ key: 'title',
+ align: 'left',
+ width: 200,
+ },
+ {
+ title: '测试分类',
+ key: 'categoryId',
+ align: 'left',
+ width: 100,
+ render(row: State) {
+ return renderOptionTag('testCategoryOption', row.categoryId);
+ },
+ },
+ {
+ title: '描述',
+ key: 'description',
+ align: 'left',
+ width: 300,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ align: 'left',
+ width: 150,
+ render(row: State) {
+ return renderOptionTag('sys_normal_disable', row.status);
+ },
+ },
+ {
+ title: '创建者',
+ key: 'createdBy',
+ align: 'left',
+ width: 100,
+ render(row: State) {
+ return renderPopoverMemberSumma(row.createdBySumma);
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ align: 'left',
+ width: 180,
+ },
+];
+
+// 加载字典数据选项
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable', 'testCategoryOption']);
+}
+
+// 关系树选项
+export const treeOption = ref([]);
+
+// 加载关系树选项
+export function loadTreeOption() {
+ TreeOption().then((res) => {
+ treeOption.value = res;
+ });
+}
\ No newline at end of file
diff --git a/web/src/views/optionTreeDemo/edit.vue b/web/src/views/optionTreeDemo/edit.vue
new file mode 100644
index 0000000..4749d0e
--- /dev/null
+++ b/web/src/views/optionTreeDemo/edit.vue
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/optionTreeDemo/index.vue b/web/src/views/optionTreeDemo/index.vue
new file mode 100644
index 0000000..c87322a
--- /dev/null
+++ b/web/src/views/optionTreeDemo/index.vue
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+ {{ expandedKeys.length ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正在编辑 {{ selectedState.title }}
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/optionTreeDemo/model.ts b/web/src/views/optionTreeDemo/model.ts
new file mode 100644
index 0000000..b407f70
--- /dev/null
+++ b/web/src/views/optionTreeDemo/model.ts
@@ -0,0 +1,165 @@
+import { h, ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { renderOptionTag, renderPopoverMemberSumma, MemberSumma } from '@/utils';
+import { TreeOption } from '@/api/optionTreeDemo';
+import { useDictStore } from '@/store/modules/dict';
+
+const dict = useDictStore();
+
+export class State {
+
+ public title = '';//标题
+ public id = 0;//ID
+ public pid = 0;//上级
+ public level = 1;//关系树级别
+ public tree = null;//关系树
+ public categoryId = null;//测试分类
+ public description = '';//描述
+ public sort = 0;//排序
+ public status = 1;//状态
+ public createdBy = 0;//创建者
+ public createdBySumma?: null | MemberSumma = null;//创建者摘要信息
+ public updatedBy = 0;//更新者
+ public createdAt = '';//创建时间
+ public updatedAt = '';//修改时间
+ public deletedAt = '';//删除时间
+ constructor(state?: Partial) {
+ if (state) {
+ Object.assign(this, state);
+ }
+ }
+}
+
+export function newState(state: State | Record | 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: '请输入标题',
+ },
+};
+
+// 表格搜索表单
+export const schemas = ref([
+ {
+ field: 'title',
+ component: 'NInput',
+ label: '标题',
+ componentProps: {
+ placeholder: '请输入标题',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'categoryId',
+ component: 'NSelect',
+ label: '测试分类',
+ defaultValue: null,
+ componentProps: {
+ placeholder: '请选择测试分类',
+ options: dict.getOption('testCategoryOption'),
+ 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);
+ },
+ },
+ },
+ {
+ field: 'createdAt',
+ component: 'NDatePicker',
+ label: '创建时间',
+ componentProps: {
+ type: 'datetimerange',
+ clearable: true,
+ shortcuts: defRangeShortcuts(),
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+]);
+
+// 表格列
+export const columns = [
+ {
+ title: '标题',
+ key: 'title',
+ align: 'left',
+ width: 100,
+ },
+ {
+ title: '测试分类',
+ key: 'categoryId',
+ align: 'left',
+ width: 100,
+ render(row: State) {
+ return renderOptionTag('testCategoryOption', row.categoryId);
+ },
+ },
+ {
+ title: '状态',
+ key: 'status',
+ align: 'left',
+ width: 150,
+ render(row: State) {
+ return renderOptionTag('sys_normal_disable', row.status);
+ },
+ },
+ {
+ title: '创建者',
+ key: 'createdBy',
+ align: 'left',
+ width: 100,
+ render(row: State) {
+ return renderPopoverMemberSumma(row.createdBySumma);
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ align: 'left',
+ width: 180,
+ },
+];
+
+// 加载字典数据选项
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable', 'testCategoryOption']);
+}
+
+// 关系树选项
+export const treeOption = ref([]);
+
+// 加载关系树选项
+export function loadTreeOption() {
+ TreeOption().then((res) => {
+ treeOption.value = res;
+ });
+}
\ No newline at end of file
diff --git a/web/src/views/org/dept/dept.vue b/web/src/views/org/dept/dept.vue
new file mode 100644
index 0000000..e1faea6
--- /dev/null
+++ b/web/src/views/org/dept/dept.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加部门
+
+
+
+
+
+
+
+ 批量删除
+
+
+ 全部{{ expandedKeys.length ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/dept/edit.vue b/web/src/views/org/dept/edit.vue
new file mode 100644
index 0000000..c1aacdb
--- /dev/null
+++ b/web/src/views/org/dept/edit.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/dept/model.ts b/web/src/views/org/dept/model.ts
new file mode 100644
index 0000000..30fc87e
--- /dev/null
+++ b/web/src/views/org/dept/model.ts
@@ -0,0 +1,201 @@
+import { h, ref } from 'vue';
+import { NTag, NButton } from 'naive-ui';
+import { cloneDeep } from 'lodash-es';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { validate } from '@/utils/validateUtil';
+import { renderTooltip, renderIcon, renderOptionTag } from '@/utils';
+import { HelpCircleOutline } from '@vicons/ionicons5';
+import { TreeOption } from '@/api/org/dept';
+import { useDictStore } from '@/store/modules/dict';
+import type { FormRules } from 'naive-ui/es/form/src/interface';
+
+const dict = useDictStore();
+
+export class State {
+ public id = 0; // 部门ID
+ public pid = 0; // 父部门ID
+ public name = ''; // 部门名称
+ public code = ''; // 部门编码
+ public type = 'company'; // 部门类型
+ public leader = ''; // 负责人
+ public phone = ''; // 联系电话
+ public email = ''; // 邮箱
+ public level = 0; // 关系树等级
+ public tree = ''; // 关系树
+ public sort = 0; // 排序
+ public status = 1; // 部门状态
+ public createdAt = ''; // 创建时间
+ public updatedAt = ''; // 更新时间
+
+ constructor(state?: Partial) {
+ if (state) {
+ Object.assign(this, state);
+ }
+ }
+}
+
+export function newState(state: State | Record | null): State {
+ if (state !== null) {
+ if (state instanceof State) {
+ return cloneDeep(state);
+ }
+ return new State(state);
+ }
+ return new State();
+}
+
+// 表单验证规则
+export const rules: FormRules = {
+ email: {
+ required: false,
+ trigger: ['blur', 'input'],
+ type: 'string',
+ validator: validate.email,
+ },
+};
+
+// 表格搜索表单
+export const schemas = ref([
+ {
+ field: 'name',
+ component: 'NInput',
+ label: '部门名称',
+ componentProps: {
+ placeholder: '请输入部门名称',
+ onInput: (e: any) => {
+ console.log(e);
+ },
+ },
+ rules: [{ message: '请输入部门名称', trigger: ['blur'] }],
+ },
+ {
+ field: 'code',
+ component: 'NInput',
+ label: '部门编码',
+ componentProps: {
+ placeholder: '请输入部门编码',
+ showButton: false,
+ onInput: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'leader',
+ component: 'NInput',
+ label: '负责人',
+ componentProps: {
+ placeholder: '请输入负责人',
+ showButton: false,
+ onInput: (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 filterIds = ref([]);
+
+// 表格列
+export const columns = [
+ {
+ title(_column) {
+ return renderTooltip(
+ h(
+ NButton,
+ {
+ strong: true,
+ size: 'small',
+ text: true,
+ iconPlacement: 'right',
+ },
+ { default: () => '部门名称', icon: renderIcon(HelpCircleOutline) }
+ ),
+ '支持上下级部门,点击列表中左侧 > 按钮可展开下级部门列表'
+ );
+ },
+ key: 'name',
+ render(row) {
+ const filter = filterIds.value.includes(row.id as never);
+ return h(
+ NTag,
+ {
+ type: 'info',
+ checkable: filter,
+ checked: filter,
+ },
+ {
+ default: () => row.name,
+ }
+ );
+ },
+ width: 200,
+ },
+ {
+ title: '部门编码',
+ key: 'code',
+ width: 100,
+ },
+ {
+ title: '部门类型',
+ key: 'type',
+ align: 'left',
+ width: 100,
+ render(row) {
+ return renderOptionTag('deptType', row.type);
+ },
+ },
+ {
+ title: '负责人',
+ key: 'leader',
+ width: 100,
+ },
+ {
+ title: '联系电话',
+ key: 'phone',
+ width: 150,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ align: 'left',
+ width: 80,
+ render(row) {
+ return renderOptionTag('sys_normal_disable', row.status);
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 150,
+ },
+];
+
+// 加载字典数据选项
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable', 'deptType']);
+}
+
+// 关系树选项
+export const treeOption = ref([]);
+
+// 加载关系树选项
+export function loadTreeOption() {
+ TreeOption().then((res) => {
+ treeOption.value = res;
+ });
+}
diff --git a/web/src/views/org/post/columns.ts b/web/src/views/org/post/columns.ts
new file mode 100644
index 0000000..a6992fe
--- /dev/null
+++ b/web/src/views/org/post/columns.ts
@@ -0,0 +1,61 @@
+import { h } from 'vue';
+import { NTag } from 'naive-ui';
+
+export const columns = [
+ {
+ title: 'ID',
+ key: 'id',
+ width: 100,
+ },
+ {
+ title: '岗位',
+ key: 'name',
+ width: 200,
+ render(row) {
+ return h(
+ NTag,
+ {
+ type: 'info',
+ },
+ {
+ default: () => row.name,
+ }
+ );
+ },
+ },
+ {
+ title: '岗位编码',
+ key: 'code',
+ width: 100,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 100,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status == 1 ? 'info' : 'error',
+ bordered: false,
+ },
+ {
+ default: () => (row.status == 1 ? '正常' : '已禁用'),
+ }
+ );
+ },
+ },
+ {
+ title: '备注',
+ key: 'sort',
+ width: 150,
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 180,
+ },
+];
diff --git a/web/src/views/org/post/post.vue b/web/src/views/org/post/post.vue
new file mode 100644
index 0000000..7d61d30
--- /dev/null
+++ b/web/src/views/org/post/post.vue
@@ -0,0 +1,305 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加岗位
+
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/user/addBalance.vue b/web/src/views/org/user/addBalance.vue
new file mode 100644
index 0000000..e552f9b
--- /dev/null
+++ b/web/src/views/org/user/addBalance.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+ 通过扣除或增加你的余额来为
+ {{ params.realName }} 加款或扣款。当扣款方余额不足时,则会操作失败
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/user/addIntegral.vue b/web/src/views/org/user/addIntegral.vue
new file mode 100644
index 0000000..ff28412
--- /dev/null
+++ b/web/src/views/org/user/addIntegral.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+ 通过扣除或增加你的积分来为
+ {{ params.realName }} 加款或扣款。当扣款方积分不足时,则会操作失败
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/user/columns.ts b/web/src/views/org/user/columns.ts
new file mode 100644
index 0000000..3638eb9
--- /dev/null
+++ b/web/src/views/org/user/columns.ts
@@ -0,0 +1,148 @@
+import { h } from 'vue';
+import { NAvatar, NTag, NText } from 'naive-ui';
+import { formatBefore } from '@/utils/dateUtil';
+
+export const columns = [
+ {
+ title: '管理员ID',
+ key: 'id',
+ width: 100,
+ },
+ {
+ title: '用户名',
+ key: 'username',
+ width: 100,
+ },
+ {
+ title: '姓名',
+ key: 'realName',
+ width: 100,
+ render(row) {
+ if (row.realName == '') {
+ return h(NText, { depth: 3 }, { default: () => '未设置' });
+ }
+ return row.realName;
+ },
+ },
+ {
+ title: '头像',
+ key: 'avatar',
+ width: 70,
+ render(row) {
+ if (row.avatar !== '') {
+ return h(NAvatar, {
+ circle: true,
+ size: 'small',
+ src: row.avatar,
+ });
+ } else {
+ return h(
+ NAvatar,
+ {
+ circle: true,
+ size: 'small',
+ },
+ {
+ default: () =>
+ row.realName !== '' ? row.realName.substring(0, 1) : row.username.substring(0, 2),
+ }
+ );
+ }
+ },
+ },
+ {
+ title: '绑定角色',
+ key: 'roleName',
+ width: 100,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'success',
+ bordered: false,
+ },
+ {
+ default: () => row.roleName,
+ }
+ );
+ },
+ },
+ {
+ title: '所属部门',
+ key: 'deptName',
+ width: 100,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'info',
+ bordered: false,
+ },
+ {
+ default: () => row.deptName,
+ }
+ );
+ },
+ },
+ {
+ title: '余额',
+ key: 'balance',
+ width: 120,
+ render(row) {
+ return '¥' + Number(row.balance).toFixed(2);
+ },
+ },
+ {
+ title: '积分',
+ key: 'integral',
+ width: 120,
+ render(row) {
+ return Number(row.integral).toFixed(2);
+ },
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 80,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status == 1 ? 'info' : 'error',
+ bordered: false,
+ },
+ {
+ default: () => (row.status == 1 ? '正常' : '已禁用'),
+ }
+ );
+ },
+ },
+ {
+ title: '最近活跃',
+ key: 'lastActiveAt',
+ width: 100,
+ render(row) {
+ if (row.lastActiveAt === null) {
+ return '从未登录';
+ }
+ return formatBefore(new Date(row.lastActiveAt));
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 180,
+ render: (rows, _) => {
+ return rows.createdAt;
+ },
+ },
+];
diff --git a/web/src/views/org/user/list.vue b/web/src/views/org/user/list.vue
new file mode 100644
index 0000000..22336e5
--- /dev/null
+++ b/web/src/views/org/user/list.vue
@@ -0,0 +1,526 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加用户
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+ 邀请注册
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 填写更多信息(可选)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showQrModal = false)">关闭
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/org/user/model.ts b/web/src/views/org/user/model.ts
new file mode 100644
index 0000000..4f18199
--- /dev/null
+++ b/web/src/views/org/user/model.ts
@@ -0,0 +1,237 @@
+import { cloneDeep } from 'lodash-es';
+import { ref } from 'vue';
+import { getDeptOption } from '@/api/org/dept';
+import { getRoleOption } from '@/api/system/role';
+import { FormSchema, useForm } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { useDictStore, Option } from '@/store/modules/dict';
+
+// 增加余额/积分.
+
+const dict = useDictStore();
+
+export interface addState {
+ id: number;
+ username: string;
+ realName: string;
+ integral: number;
+ balance: number;
+ operateMode: number;
+ num: number | null;
+}
+
+export const addDefaultState = {
+ id: 0,
+ realName: '',
+ username: '',
+ integral: 0,
+ balance: 0,
+ operateMode: 1,
+ num: null,
+};
+
+export function addNewState(state: addState | null): addState {
+ if (state !== null) {
+ return cloneDeep(state);
+ }
+ return cloneDeep(addDefaultState);
+}
+
+export const operateModes = [
+ {
+ value: 1,
+ label: '加款',
+ },
+ {
+ value: 2,
+ label: '扣款',
+ },
+];
+
+export const addRules = {};
+
+// 用户列表.
+
+export const defaultState = {
+ id: 0,
+ roleId: null,
+ realName: '',
+ username: '',
+ password: '',
+ deptId: null,
+ postIds: null,
+ mobile: '',
+ email: '',
+ sex: 1,
+ leader: '',
+ phone: '',
+ sort: 0,
+ status: 1,
+ createdAt: '',
+ updatedAt: '',
+};
+
+export interface State {
+ id: number;
+ roleId: number | null;
+ realName: string;
+ username: string;
+ password: string;
+ deptId: number | null;
+ postIds: any;
+ mobile: string;
+ email: string;
+ sex: number;
+ leader: string;
+ phone: string;
+ sort: number;
+ status: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+const schemas: FormSchema[] = [
+ {
+ field: 'username',
+ component: 'NInput',
+ label: '用户名',
+ componentProps: {
+ placeholder: '请输入用户名',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ rules: [{ message: '请输入用户名', trigger: ['blur'] }],
+ },
+ {
+ field: 'realName',
+ component: 'NInput',
+ label: '姓名',
+ componentProps: {
+ placeholder: '请输入姓名',
+ showButton: false,
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'mobile',
+ component: 'NInputNumber',
+ label: '手机号',
+ componentProps: {
+ placeholder: '请输入手机号码',
+ showButton: false,
+ onInput: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'email',
+ component: 'NInput',
+ label: '邮箱',
+ componentProps: {
+ placeholder: '请输入邮箱地址',
+ onInput: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'id',
+ component: 'NInput',
+ label: '用户ID',
+ componentProps: {
+ placeholder: '请输入用户ID',
+ onInput: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'pid',
+ component: 'NInput',
+ label: '上级ID',
+ componentProps: {
+ placeholder: '请输入上级用户ID',
+ onInput: (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);
+ },
+ },
+ },
+ {
+ field: 'created_at',
+ component: 'NDatePicker',
+ label: '创建时间',
+ componentProps: {
+ type: 'datetimerange',
+ clearable: true,
+ shortcuts: defRangeShortcuts(),
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+];
+
+export const [register, {}] = useForm({
+ gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
+ labelWidth: 80,
+ schemas,
+});
+
+export const deptTreeOptions = ref([]);
+export const roleTreeOptions = ref([]);
+
+export async function loadOptions() {
+ dict.loadOptions(['adminPostOption', 'sys_user_sex', 'sys_normal_disable']);
+
+ getDeptOption().then((res) => {
+ if (res.list) {
+ deptTreeOptions.value = res.list;
+ }
+ });
+
+ getRoleOption().then((res) => {
+ if (res.list) {
+ roleTreeOptions.value = res.list;
+ registerRoleTabsOption();
+ }
+ });
+}
+
+// 将角色Tabs选项注册到字典
+function registerRoleTabsOption() {
+ const items = [{ id: -1, name: '全部' }];
+ treeDataToCompressed(items, roleTreeOptions.value);
+
+ const roleTabs: Option[] = [];
+ for (const item of items) {
+ roleTabs.push(dict.genOption(item.id, item.name));
+ }
+ dict.setOption('roleTabs', roleTabs);
+}
+
+function treeDataToCompressed(items: any[], source: any) {
+ for (const i in source) {
+ items.push(source[i]);
+ source[i].children && source[i].children.length > 0
+ ? treeDataToCompressed(items, source[i].children)
+ : ''; // 子级递归
+ }
+ return items;
+}
diff --git a/web/src/views/org/user/user.vue b/web/src/views/org/user/user.vue
new file mode 100644
index 0000000..1f3986d
--- /dev/null
+++ b/web/src/views/org/user/user.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/menu/addModal.vue b/web/src/views/permission/menu/addModal.vue
new file mode 100644
index 0000000..26f49c6
--- /dev/null
+++ b/web/src/views/permission/menu/addModal.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+ 关闭
+ 重置
+ 确定添加
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/menu/editForm.vue b/web/src/views/permission/menu/editForm.vue
new file mode 100644
index 0000000..efa31b5
--- /dev/null
+++ b/web/src/views/permission/menu/editForm.vue
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请填写图标编码,可以参考图标库,也可以不填使用默认图标
+
+ 菜单图标
+
+
+
+
+
+
+
+
+
+
+
+ 请路由地址,如:user
+
+ 路由地址
+
+
+
+
+
+
+
+
+
+
+
+ 对应路由配置文件中 `name` 只能是唯一性,配置 `http(s)://` 开头地址 则会新窗口打开
+
+ 路由别名
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 填 Vue 组件路径,如:`/system/menu/menu`
+
+
+
+
+
+ 当目录下存在多个同级菜单时适用,如:`/system/menu/menu`
+
+
+
+
+ 功能设置
+
+
+
+
+
+
+
+
+
+
+ 请填写API路径地址,可同时作用于server端接口鉴权和web端细粒度权限
+
+ 分配权限
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存修改
+ 重置
+ 删除
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/menu/menu.vue b/web/src/views/permission/menu/menu.vue
new file mode 100644
index 0000000..7da0a46
--- /dev/null
+++ b/web/src/views/permission/menu/menu.vue
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加菜单
+
+
+
+
+
+ 添加子菜单
+
+
+ 全部{{ expandedKeys.length ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑菜单{{ treeItemTitle ? `:${treeItemTitle}` : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/menu/menuModal.vue b/web/src/views/permission/menu/menuModal.vue
new file mode 100644
index 0000000..16405bc
--- /dev/null
+++ b/web/src/views/permission/menu/menuModal.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/menu/model.ts b/web/src/views/permission/menu/model.ts
new file mode 100644
index 0000000..44929ee
--- /dev/null
+++ b/web/src/views/permission/menu/model.ts
@@ -0,0 +1,102 @@
+import { cloneDeep } from 'lodash-es';
+import { useDictStore } from '@/store/modules/dict';
+
+const dict = useDictStore();
+
+export interface State {
+ id: number;
+ pid?: number | null;
+ title: string;
+ name: string;
+ path: string;
+ label: string;
+ icon: string;
+ type: number;
+ redirect: string;
+ permissions: string;
+ permissionName: string;
+ component?: string | null;
+ alwaysShow: number;
+ activeMenu: string;
+ isRoot: number;
+ isFrame: number;
+ frameSrc: string;
+ keepAlive: number;
+ hidden: number;
+ affix: number;
+ status: number;
+ sort: number;
+ disabled: boolean;
+ children?: State[] | null;
+}
+
+export const defaultState: State = {
+ id: 0,
+ pid: 0,
+ title: '',
+ name: '',
+ path: '',
+ label: '',
+ icon: '',
+ type: 1,
+ redirect: '',
+ permissions: '',
+ permissionName: '',
+ component: null,
+ alwaysShow: 2,
+ activeMenu: '',
+ isRoot: 2,
+ isFrame: 2,
+ frameSrc: '',
+ keepAlive: 2,
+ hidden: 2,
+ affix: 2,
+ status: 1,
+ sort: 10,
+ disabled: false,
+ children: null,
+};
+
+export function newState(state: State | null): State {
+ if (state !== null) {
+ return defaultValueCheck(cloneDeep(state));
+ }
+ return defaultValueCheck(cloneDeep(defaultState));
+}
+
+// 默认值校正,主要为了解决历史数据格式不规范问题
+export function defaultValueCheck(state: State): State {
+ if (!state) {
+ state = newState(null);
+ }
+ if (!state.pid || state.pid < 1) {
+ state.pid = null;
+ }
+ if (state.alwaysShow != 1) {
+ state.alwaysShow = 2;
+ }
+ if (state.isRoot != 1) {
+ state.isRoot = 2;
+ }
+ if (state.isFrame != 1) {
+ state.isFrame = 2;
+ }
+ if (state.keepAlive != 1) {
+ state.keepAlive = 2;
+ }
+ if (state.hidden != 1) {
+ state.hidden = 2;
+ }
+ if (state.affix != 1) {
+ state.affix = 2;
+ }
+ if (state.status != 1) {
+ state.status = 2;
+ }
+ return state;
+}
+
+// 加载字典数据选项
+export function loadOptions() {
+ dict.loadOptions(['sys_menu_types', 'sys_menu_component', 'sys_normal_disable', 'sys_switch']);
+}
diff --git a/web/src/views/permission/role/columns.ts b/web/src/views/permission/role/columns.ts
new file mode 100644
index 0000000..0d3caf3
--- /dev/null
+++ b/web/src/views/permission/role/columns.ts
@@ -0,0 +1,100 @@
+import { h } from 'vue';
+import { NTag, NButton } from 'naive-ui';
+import { HelpCircleOutline } from '@vicons/ionicons5';
+import { renderTooltip, renderIcon } from '@/utils';
+export const columns = [
+ // {
+ // title: '角色ID',
+ // key: 'id',
+ // },
+ {
+ title(_column) {
+ return renderTooltip(
+ h(
+ NButton,
+ {
+ strong: true,
+ size: 'small',
+ text: true,
+ iconPlacement: 'right',
+ },
+ { default: () => '角色', icon: renderIcon(HelpCircleOutline) }
+ ),
+ '支持上下级角色,点击列表中左侧 > 按钮可展开下级角色列表'
+ );
+ },
+ key: 'name',
+ render(row) {
+ return h(
+ NTag,
+ {
+ type: 'info',
+ },
+ {
+ default: () => row.name,
+ }
+ );
+ },
+ width: 200,
+ },
+ {
+ title: '角色编码',
+ key: 'key',
+ width: 150,
+ },
+ // {
+ // title: '上级角色',
+ // key: 'pid',
+ // },
+ // {
+ // title: '默认角色',
+ // key: 'isDefault',
+ // render(row) {
+ // return h(
+ // NTag,
+ // {
+ // type: row.id == 1 ? 'success' : 'error',
+ // },
+ // {
+ // default: () => (row.id == 1 ? '是' : '否'),
+ // }
+ // );
+ // },
+ // width: 80,
+ // },
+ // {
+ // title: '排序',
+ // key: 'sort',
+ // width: 100,
+ // },
+ {
+ title: '备注',
+ key: 'remark',
+ width: 180,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ width: 80,
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status == 1 ? 'info' : 'error',
+ bordered: false,
+ },
+ {
+ default: () => (row.status == 1 ? '正常' : '已禁用'),
+ }
+ );
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 180,
+ },
+];
diff --git a/web/src/views/permission/role/editDataAuth.vue b/web/src/views/permission/role/editDataAuth.vue
new file mode 100644
index 0000000..ccaaf8b
--- /dev/null
+++ b/web/src/views/permission/role/editDataAuth.vue
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/role/editMenuAuth.vue b/web/src/views/permission/role/editMenuAuth.vue
new file mode 100644
index 0000000..44710fb
--- /dev/null
+++ b/web/src/views/permission/role/editMenuAuth.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+
+
+ 导入权限
+
+ 全部{{ expandedKeys.length ? '收起' : '展开' }}
+
+
+ 全部{{ checkedAll ? '取消' : '选择' }}
+
+
+
+
+ 提交
+
+ 你正在修改 {{ formValue.name }} 的菜单权限,确定要提交吗?
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/role/editRole.vue b/web/src/views/permission/role/editRole.vue
new file mode 100644
index 0000000..efa2b69
--- /dev/null
+++ b/web/src/views/permission/role/editRole.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/permission/role/model.ts b/web/src/views/permission/role/model.ts
new file mode 100644
index 0000000..7fd503c
--- /dev/null
+++ b/web/src/views/permission/role/model.ts
@@ -0,0 +1,40 @@
+import { cloneDeep } from 'lodash-es';
+
+export class State {
+ id = 0;
+ name = '';
+ key = '';
+ dataScope = 1;
+ customDept: any[] = [];
+ pid = 0;
+ level = 1;
+ tree = '';
+ remark = '';
+ sort = 0;
+ status = 1;
+ createdAt = '';
+ updatedAt = '';
+ label = '';
+ value = 0;
+ children: State[] | null = null;
+
+ constructor(state?: Partial) {
+ if (state) {
+ Object.keys(this).forEach((key) => {
+ if (state[key] !== undefined) {
+ this[key] = state[key];
+ }
+ });
+ }
+ }
+}
+
+export function newState(state: State | Record | null): State {
+ if (state !== null) {
+ if (state instanceof State) {
+ return cloneDeep(state);
+ }
+ return new State(state);
+ }
+ return new State();
+}
diff --git a/web/src/views/permission/role/role.vue b/web/src/views/permission/role/role.vue
new file mode 100644
index 0000000..fab8c6d
--- /dev/null
+++ b/web/src/views/permission/role/role.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+ 添加角色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/redirect/index.vue b/web/src/views/redirect/index.vue
new file mode 100644
index 0000000..175e0ae
--- /dev/null
+++ b/web/src/views/redirect/index.vue
@@ -0,0 +1,22 @@
+
diff --git a/web/src/views/system/blacklist/index.vue b/web/src/views/system/blacklist/index.vue
new file mode 100644
index 0000000..00e6fbc
--- /dev/null
+++ b/web/src/views/system/blacklist/index.vue
@@ -0,0 +1,344 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加策略
+
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+ 支持添加IP:如果添加多个IP请用","隔开
+ 支持添加IP段,如:192.168.0.0/24
+ 支持添加IP范围,格式如:192.168.1.xx-192.168.1.xx
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/BasicSetting.vue b/web/src/views/system/config/BasicSetting.vue
new file mode 100644
index 0000000..13c8338
--- /dev/null
+++ b/web/src/views/system/config/BasicSetting.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 如果客户端通过本地或内网访问,则认为是调试模式,走实际请求地址,否则走该配置
+
+
+
+
+
+
+ 如果客户端通过本地或内网访问,则认为是调试模式,走实际请求地址,否则走该配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/CashSetting.vue b/web/src/views/system/config/CashSetting.vue
new file mode 100644
index 0000000..c005e4c
--- /dev/null
+++ b/web/src/views/system/config/CashSetting.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+ 开启
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/EmailSetting.vue b/web/src/views/system/config/EmailSetting.vue
new file mode 100644
index 0000000..51707a7
--- /dev/null
+++ b/web/src/views/system/config/EmailSetting.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+ 错误的配置发送邮件会导致服务器超时
+
+
+
+
+ (不加密默认25,SSL默认465,TLS默认587)
+
+
+
+ 填写完整用户名
+
+
+
+
+
+
+
+
+
+
+
+ 填写您的密码
+
+
+
+
+
+
+
+
+
+
+ 发信限制
+
+
+ 秒
+
+ 同地址
+
+
+
+ 同IP每天最大允许发送次数
+
+
+
+ 秒
+
+
+
+
+
+
+
+
+
+ 保存更新
+ 发送测试邮件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">关闭
+ 发送
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/GeoSetting.vue b/web/src/views/system/config/GeoSetting.vue
new file mode 100644
index 0000000..365883c
--- /dev/null
+++ b/web/src/views/system/config/GeoSetting.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 申请地址:https://console.amap.com/dev/key/app
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/LoginSetting.vue b/web/src/views/system/config/LoginSetting.vue
new file mode 100644
index 0000000..998ec64
--- /dev/null
+++ b/web/src/views/system/config/LoginSetting.vue
@@ -0,0 +1,208 @@
+
+
+
+
+ 开关配置
+
+
+
+ 开启
+ 关闭
+
+
+
+
+
+
+
+ 字符
+ 算数
+
+
+
+
+
+
+
+ 开启
+ 关闭
+
+
+
+
+
+
+
+ 开启
+ 关闭
+
+
+
+ 用户通过注册页面发起注册时是否必须填写邀请信息,用于上下级关系绑定
+
+
+
+
+
+ 开启
+ 关闭
+
+
+
+ 在微信内登录后台时,自动获取当前登录人的openid(如开启需要配置微信公众号参数)
+
+
+ 注册默认信息配置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 协议配置
+
+
+
+
+
+
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/PaySetting.vue b/web/src/views/system/config/PaySetting.vue
new file mode 100644
index 0000000..f7715fb
--- /dev/null
+++ b/web/src/views/system/config/PaySetting.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+ 开启后控制台会输出支付相关的日志
+
+
+ 支付宝
+
+ 确保你已经申请开通过支付宝相关产品权限,建议按照以下步骤进行配置
+
1.
+ 下载支付宝平台密钥工具(下载地址:https://opendocs.alipay.com/common/02kipk),加签方式选择证书,加密算法选择RSA2
+
2. 生成后的私钥请在工具中转换为PKCS1格式
3.
+ 在支付宝中配置证书,参考地址:https://opendocs.alipay.com/common/02khjo?pathHash=5403bedd
+
+
+
+
+
+
+
+
+ RSA2 加密算法默认生成格式为 PKCS8,系统默认是RSA2加密,切记转换为 PKCS1 格式
+
+
+
+
+ appCertPublicKey.crt证书路径
+
+
+
+
+ alipayRootCert.crt证书路径"
+
+
+
+
+ alipayCertPublicKey_RSA2.crt证书路径"
+
+
+ 微信支付
+
+
+ 和微信配置中的微信公众号配置保持一致
+
+
+
+
+ 商户ID 或者服务商模式的 sp_mchid
+
+
+
+
+ 商户证书的证书序列号
+
+
+
+ 商户平台获取
+
+
+
+
+ apiclient_key.pem 读取后的内容
+
+
+ QQ支付
+
+
+
+
+
+
+
+
+
+
+
+
+ API秘钥值
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">关闭
+ 发送
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/RevealSetting.vue b/web/src/views/system/config/RevealSetting.vue
new file mode 100644
index 0000000..363997b
--- /dev/null
+++ b/web/src/views/system/config/RevealSetting.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+ 宽度:
+
+ 高度:
+
+
+
+
+
+
+ 宽度:
+
+ 高度:
+
+
+
+
+
+
+
+
+
+
+ 上传文件
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 更新显示信息
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/SmsSetting.vue b/web/src/views/system/config/SmsSetting.vue
new file mode 100644
index 0000000..9c1755d
--- /dev/null
+++ b/web/src/views/system/config/SmsSetting.vue
@@ -0,0 +1,322 @@
+
+
+
+
+ 基础设置
+
+
+
+
+
+ 秒
+
+ 同号码
+
+
+
+ 同IP每天最大允许发送次数
+
+
+
+ 秒
+
+
+
+
+
+ 阿里云
+ 阿里云
+
+
+
+ 应用key和密钥你可以通过 https://ram.console.aliyun.com/manage/ak 获取
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 申请地址:https://dysms.console.aliyun.com/domestic/text/sign
+
+
+
+
+
+
+
+
+
+ 腾讯云
+ 腾讯云
+
+
+
+ 子账号密钥获取地址:https://cloud.tencent.com/document/product/598/37140
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 默认就近地域接入域名为 sms.tencentcloudapi.com
+ ,也支持指定地域域名访问,例如广州地域的域名为
+ sms.ap-guangzhou.tencentcloudapi.com
+
+
+
+
+
+
+ 支持的地域列表参考
+ https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
+
+
+
+
+
+
+ 查看地址:https://console.cloud.tencent.com/smsv2/app-manage
+
+
+
+
+
+
+ 查看地址:https://console.cloud.tencent.com/smsv2/csms-sign
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存更新
+ 发送测试短信
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">关闭
+ 发送
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/ThemeSetting.vue b/web/src/views/system/config/ThemeSetting.vue
new file mode 100644
index 0000000..08baab2
--- /dev/null
+++ b/web/src/views/system/config/ThemeSetting.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+ 可选:'dark' 、 'light'
+
+
+
+
+ 默认:#2d8cf0
+
+
+
+ 可选:'light'、 'header-dark'
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/UploadSetting.vue b/web/src/views/system/config/UploadSetting.vue
new file mode 100644
index 0000000..2ad5573
--- /dev/null
+++ b/web/src/views/system/config/UploadSetting.vue
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 填对外访问的相对路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 创建地址:https://ram.console.aliyun.com/manage/ak
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Endpoint(地域节点)
+
+
+
+ 填对对象存储中的相对路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 子账号密钥获取地址:https://cloud.tencent.com/document/product/598/37140
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 填对对象存储中的相对路径
+
+
+
+ 控制台查看地址:https://console.cloud.tencent.com/cos5/bucket
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 创建地址:https://portal.qiniu.com/user/key
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 填对对象存储中的相对路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 获取地址:https://console.ucloud.cn/ufile/token
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 填对对象存储中的相对路径
+
+
+
+
+
+
+ 存储空间名称
+
+
+
+ 不需要包含桶名称
+
+
+
+ 格式,http://abc.com 或 https://abc.com,不可为空
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 相关文档:https://min.io/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Endpoint(不带http://和路径)
+
+
+
+ 已启用
+ 已禁用
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MB
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/WechatSetting.vue b/web/src/views/system/config/WechatSetting.vue
new file mode 100644
index 0000000..d14fe2b
--- /dev/null
+++ b/web/src/views/system/config/WechatSetting.vue
@@ -0,0 +1,121 @@
+
+
+
+
+ 公众号
+
+
+ 请填写微信公众平台后台的AppId
+
+
+
+
+ 请填写微信公众平台后台的AppSecret
+
+
+
+
+ 与公众平台接入设置值一致,必须为英文或者数字,长度为3到32个字符
+
+
+
+
+ 与公众平台接入设置值一致,必须为英文或者数字,长度为43个字符
+
+
+
+ 开放平台
+
+
+ 请填写微信开放平台后台的AppId
+
+
+
+
+ 请填写微信开放平台后台的AppSecret
+
+
+
+
+ 与开放平台接入设置值一致,必须为英文或者数字,长度为3到32个字符
+
+
+
+
+ 与开放平台接入设置值一致,必须为英文或者数字,长度为43个字符
+
+
+
+
+
+
+ 保存更新
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/config/system.vue b/web/src/views/system/config/system.vue
new file mode 100644
index 0000000..81b8451
--- /dev/null
+++ b/web/src/views/system/config/system.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+ {{ item.name }}
+ {{ item.desc }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/cron/columns.ts b/web/src/views/system/cron/columns.ts
new file mode 100644
index 0000000..35f5c33
--- /dev/null
+++ b/web/src/views/system/cron/columns.ts
@@ -0,0 +1,93 @@
+import { h } from 'vue';
+import { NTag } from 'naive-ui';
+
+const policyOptions = {
+ 1: '并行策略',
+ 2: '单例策略',
+ 3: '单次策略',
+ 4: '多次策略',
+};
+
+export const columns = [
+ {
+ title: 'ID',
+ key: 'id',
+ width: 80,
+ },
+ {
+ title: '任务标题',
+ key: 'title',
+ width: 150,
+ },
+ {
+ title: '任务分组',
+ key: 'groupName',
+ width: 100,
+ },
+ {
+ title: '执行方法',
+ key: 'name',
+ width: 100,
+ },
+ // {
+ // title: '执行参数',
+ // key: 'params',
+ // render(row) {
+ // return row.params;
+ // },
+ // },
+ {
+ title: '执行策略',
+ key: 'policy',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'info',
+ bordered: false,
+ },
+ {
+ default: () => policyOptions[row.policy] ?? '未知',
+ }
+ );
+ },
+ width: 100,
+ },
+ {
+ title: '表达式',
+ key: 'pattern',
+ width: 150,
+ },
+ // {
+ // title: '执行次数',
+ // key: 'count',
+ // },
+ {
+ title: '状态',
+ key: 'status',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status == 1 ? 'success' : 'warning',
+ bordered: false,
+ },
+ {
+ default: () => (row.status == 1 ? '运行中' : '已结束'),
+ }
+ );
+ },
+ width: 100,
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ width: 180,
+ },
+];
diff --git a/web/src/views/system/cron/index.vue b/web/src/views/system/cron/index.vue
new file mode 100644
index 0000000..4195c5b
--- /dev/null
+++ b/web/src/views/system/cron/index.vue
@@ -0,0 +1,554 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加任务
+
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+ 任务分组
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ go方法名称
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 表达式语法参考:https://goframe.org/pages/viewpage.action?pageId=30736411
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/cron/modal/columns.ts b/web/src/views/system/cron/modal/columns.ts
new file mode 100644
index 0000000..7774dcf
--- /dev/null
+++ b/web/src/views/system/cron/modal/columns.ts
@@ -0,0 +1,83 @@
+import { h } from 'vue';
+import { NTag } from 'naive-ui';
+
+export const columns = [
+ {
+ title: 'ID',
+ dataIndex: 'id',
+ key: 'id',
+ width: 100,
+ },
+ {
+ title: '分组名称',
+ dataIndex: 'name',
+ key: 'name',
+ width: 150,
+ },
+ {
+ title: '上级分组',
+ dataIndex: 'supName',
+ key: 'supName',
+ width: 150,
+ render(row) {
+ if (row.supName == ''){
+ return '顶级分组';
+ }
+ return row.supName;
+ },
+ },
+ {
+ title: '默认',
+ dataIndex: 'isDefault',
+ key: 'isDefault',
+ render(row) {
+ return row.isDefault === 1 ? '是' : '否';
+ },
+ width: 80,
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status === 1 ? 'info' : 'error',
+ bordered: false,
+ },
+ {
+ default: () => (row.status === 1 ? '正常' : '禁用'),
+ }
+ );
+ },
+ width: 150,
+ },
+ // {
+ // title: '排序',
+ // dataIndex: 'sort',
+ // key: 'sort',
+ // width: 100,
+ // },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ key: 'remark',
+ width: 150,
+ },
+ // {
+ // title: '更新时间',
+ // dataIndex: 'updatedAt',
+ // key: 'updatedAt',
+ // width: 160,
+ // },
+ {
+ title: '创建时间',
+ dataIndex: 'createdAt',
+ key: 'createdAt',
+ width: 160,
+ },
+];
diff --git a/web/src/views/system/cron/modal/index.vue b/web/src/views/system/cron/modal/index.vue
new file mode 100644
index 0000000..8e6a1df
--- /dev/null
+++ b/web/src/views/system/cron/modal/index.vue
@@ -0,0 +1,237 @@
+
+
+
+
+
+
+
+
+
+
+
+ 添加分组
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/cron/modal/modal.vue b/web/src/views/system/cron/modal/modal.vue
new file mode 100644
index 0000000..d851f78
--- /dev/null
+++ b/web/src/views/system/cron/modal/modal.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/dict/CreateDrawer.vue b/web/src/views/system/dict/CreateDrawer.vue
new file mode 100644
index 0000000..5e8da0d
--- /dev/null
+++ b/web/src/views/system/dict/CreateDrawer.vue
@@ -0,0 +1,132 @@
+
+
+
+
+ 基本设置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 取消
+
+
+
+
+
+
+
diff --git a/web/src/views/system/dict/columns.ts b/web/src/views/system/dict/columns.ts
new file mode 100644
index 0000000..8dfaa4d
--- /dev/null
+++ b/web/src/views/system/dict/columns.ts
@@ -0,0 +1,74 @@
+import { h } from 'vue';
+import { NTag } from 'naive-ui';
+
+export const columns = [
+ {
+ title: '字典ID',
+ key: 'id',
+ },
+ {
+ title: '字典类型',
+ key: 'type',
+ },
+ {
+ title: '字典标签',
+ key: 'type',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.listClass,
+ bordered: false,
+ },
+ {
+ default: () => row.label,
+ }
+ );
+ },
+ },
+ {
+ title: '字典键值',
+ key: 'value',
+ },
+ {
+ title: '键值类型',
+ key: 'valueType',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: 'default',
+ bordered: false,
+ },
+ {
+ default: () => row.valueType,
+ }
+ );
+ },
+ },
+ {
+ title: '状态',
+ key: 'status',
+ render(row) {
+ return h(
+ NTag,
+ {
+ style: {
+ marginRight: '6px',
+ },
+ type: row.status == 1 ? 'success' : 'warning',
+ bordered: false,
+ },
+ {
+ default: () => (row.status == 1 ? '正常' : '隐藏'),
+ }
+ );
+ },
+ },
+];
diff --git a/web/src/views/system/dict/index.vue b/web/src/views/system/dict/index.vue
new file mode 100644
index 0000000..fbcbc7b
--- /dev/null
+++ b/web/src/views/system/dict/index.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+
+
+
+
+
+
+ 删除
+
+
+ {{ expandedKeys.length ? '收起' : '展开' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 编辑字典{{ treeItemTitle ? `:${treeItemTitle}` : '' }}
+ {{
+ treeItemTitle ? '' : '从列表选择一项后,进行编辑'
+ }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/dict/list.vue b/web/src/views/system/dict/list.vue
new file mode 100644
index 0000000..552ddde
--- /dev/null
+++ b/web/src/views/system/dict/list.vue
@@ -0,0 +1,325 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ (showModal = false)">取消
+ 确定
+
+
+
+
+
+
+
+
+
diff --git a/web/src/views/system/dict/model.ts b/web/src/views/system/dict/model.ts
new file mode 100644
index 0000000..de6ccf2
--- /dev/null
+++ b/web/src/views/system/dict/model.ts
@@ -0,0 +1,18 @@
+export class State {
+ typeId = 0;
+ key = '';
+ id = 0;
+ label = '';
+ value = '';
+ valueType = 'string';
+ type = '';
+ listClass = 'primary';
+ isDefault = 0;
+ sort = 0;
+ remark = '';
+ status = 1;
+ createdAt = '';
+ updatedAt = '';
+ pid: number | null = 0;
+ name = '';
+}
diff --git a/web/src/views/testCategory/edit.vue b/web/src/views/testCategory/edit.vue
new file mode 100644
index 0000000..d9f267f
--- /dev/null
+++ b/web/src/views/testCategory/edit.vue
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 确定
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/testCategory/index.vue b/web/src/views/testCategory/index.vue
new file mode 100644
index 0000000..a1b4dbd
--- /dev/null
+++ b/web/src/views/testCategory/index.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+
+
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/src/views/testCategory/model.ts b/web/src/views/testCategory/model.ts
new file mode 100644
index 0000000..ff7bd36
--- /dev/null
+++ b/web/src/views/testCategory/model.ts
@@ -0,0 +1,153 @@
+import { h, ref } from 'vue';
+import { cloneDeep } from 'lodash-es';
+import { FormSchema } from '@/components/Form';
+import { defRangeShortcuts } from '@/utils/dateUtil';
+import { renderOptionTag } from '@/utils';
+import { useDictStore } from '@/store/modules/dict';
+
+const dict = useDictStore();
+
+export class State {
+ public id = 0; // 分类ID
+ public name = ''; // 分类名称
+ public shortName = ''; // 简称
+ public description = ''; // 描述
+ public sort = 0; // 排序
+ public status = 1; // 状态
+ public remark = ''; // 备注
+ public createdAt = ''; // 创建时间
+ public updatedAt = ''; // 修改时间
+ public deletedAt = ''; // 删除时间
+
+ constructor(state?: Partial) {
+ if (state) {
+ Object.assign(this, state);
+ }
+ }
+}
+
+export function newState(state: State | Record | null): State {
+ if (state !== null) {
+ if (state instanceof State) {
+ return cloneDeep(state);
+ }
+ return new State(state);
+ }
+ return new State();
+}
+
+// 表单验证规则
+export const rules = {
+ name: {
+ required: true,
+ trigger: ['blur', 'input'],
+ type: 'string',
+ message: '请输入分类名称',
+ },
+ sort: {
+ required: true,
+ trigger: ['blur', 'input'],
+ type: 'number',
+ message: '请输入排序',
+ },
+};
+
+// 表格搜索表单
+export const schemas = ref([
+ {
+ field: 'id',
+ component: 'NInputNumber',
+ label: '分类ID',
+ componentProps: {
+ placeholder: '请输入分类ID',
+ onUpdateValue: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ {
+ field: 'name',
+ component: 'NInput',
+ label: '分类名称',
+ componentProps: {
+ placeholder: '请输入分类名称',
+ 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);
+ },
+ },
+ },
+ {
+ 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: 80,
+ },
+ {
+ title: '分类名称',
+ key: 'name',
+ align: 'left',
+ width: -1,
+ },
+ {
+ title: '简称',
+ key: 'shortName',
+ align: 'left',
+ width: 80,
+ },
+ {
+ title: '描述',
+ key: 'description',
+ align: 'left',
+ width: 300,
+ },
+ {
+ title: '状态',
+ key: 'status',
+ align: 'left',
+ width: -1,
+ render(row: State) {
+ return renderOptionTag('sys_normal_disable', row.status);
+ },
+ },
+ {
+ title: '创建时间',
+ key: 'createdAt',
+ align: 'left',
+ width: 180,
+ },
+];
+
+// 加载字典数据选项
+export function loadOptions() {
+ dict.loadOptions(['sys_normal_disable']);
+}
\ No newline at end of file
diff --git a/web/stylelint.config.js b/web/stylelint.config.js
new file mode 100644
index 0000000..06951ed
--- /dev/null
+++ b/web/stylelint.config.js
@@ -0,0 +1,70 @@
+module.exports = {
+ root: true,
+ plugins: ['stylelint-order'],
+ extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
+ rules: {
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['global'],
+ },
+ ],
+ 'selector-pseudo-element-no-unknown': [
+ true,
+ {
+ ignorePseudoElements: ['v-deep'],
+ },
+ ],
+ 'at-rule-no-unknown': [
+ true,
+ {
+ ignoreAtRules: [
+ 'tailwind',
+ 'apply',
+ 'variants',
+ 'responsive',
+ 'screen',
+ 'function',
+ 'if',
+ 'each',
+ 'include',
+ 'mixin',
+ ],
+ },
+ ],
+ 'no-empty-source': null,
+ 'named-grid-areas-no-invalid': null,
+ 'unicode-bom': 'never',
+ 'no-descending-specificity': null,
+ 'font-family-no-missing-generic-family-keyword': null,
+ 'declaration-colon-space-after': 'always-single-line',
+ 'declaration-colon-space-before': 'never',
+ // 'declaration-block-trailing-semicolon': 'always',
+ 'rule-empty-line-before': [
+ 'always',
+ {
+ ignore: ['after-comment', 'first-nested'],
+ },
+ ],
+ 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
+ 'order/order': [
+ [
+ 'dollar-variables',
+ 'custom-properties',
+ 'at-rules',
+ 'declarations',
+ {
+ type: 'at-rule',
+ name: 'supports',
+ },
+ {
+ type: 'at-rule',
+ name: 'media',
+ },
+ 'rules',
+ ],
+ { severity: 'warning' },
+ ],
+ },
+ ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
+};
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
new file mode 100644
index 0000000..ed05a3d
--- /dev/null
+++ b/web/tailwind.config.js
@@ -0,0 +1,7 @@
+module.exports = {
+ content: ['./index.html', './src/**/*.{vue,ts,tsx}'],
+ important: true,
+ theme: {
+ extend: {},
+ },
+};
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..12b3587
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,60 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowSyntheticDefaultImports": true,
+ "strictFunctionTypes": false,
+ "jsx": "preserve",
+ "baseUrl": ".",
+ "allowJs": true,
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "experimentalDecorators": true,
+ "lib": [
+ "dom",
+ "esnext"
+ ],
+ "types": [
+ "vite/client"
+ ],
+ "typeRoots": [
+ "./node_modules/@types/",
+ "./types"
+ ],
+ "noImplicitAny": false,
+ "skipLibCheck": true,
+ "paths": {
+ "@/*": [
+ "src/*"
+ ],
+ "/#/*": [
+ "types/*"
+ ]
+ }
+ },
+ "include": [
+ "src/**/*.ts",
+ "src/**/*.d.ts",
+ "src/**/*.tsx",
+ "src/**/*.vue",
+ "types/**/*.d.ts",
+ "types/**/*.ts",
+ "build/**/*.ts",
+ "build/**/*.d.ts",
+ "mock/**/*.ts",
+ "components.d.ts",
+ "auto-imports.d.ts",
+ "vite.config.ts"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.js"
+ ]
+}
diff --git a/web/types/config.d.ts b/web/types/config.d.ts
new file mode 100644
index 0000000..f326389
--- /dev/null
+++ b/web/types/config.d.ts
@@ -0,0 +1,71 @@
+export interface ProjectSettingState {
+ //导航模式
+ navMode: string;
+ //导航风格
+ navTheme: string;
+ //顶部设置
+ headerSetting: object;
+ //页脚
+ showFooter: boolean;
+ //菜单设置
+ menuSetting: object;
+ //多标签
+ multiTabsSetting: object;
+ //面包屑
+ crumbsSetting: object;
+ //权限模式
+ permissionMode: string;
+}
+
+export interface IbodySetting {
+ fixed: boolean;
+}
+
+export interface IheaderSetting {
+ bgColor: string;
+ fixed: boolean;
+ isReload: boolean;
+}
+
+export interface ImenuSetting {
+ minMenuWidth: number;
+ menuWidth: number;
+ fixed: boolean;
+ mixMenu: boolean;
+ collapsed: boolean;
+ mobileWidth: number;
+}
+
+export interface IcrumbsSetting {
+ show: boolean;
+ showIcon: boolean;
+}
+
+export interface ImultiTabsSetting {
+ bgColor: string;
+ fixed: boolean;
+ show: boolean;
+}
+export interface GlobConfig {
+ title: string;
+ apiUrl: string;
+ shortName: string;
+ urlPrefix?: string;
+ uploadUrl?: string;
+ imgUrl?: string;
+}
+
+export interface GlobEnvConfig {
+ // 标题
+ VITE_GLOB_APP_TITLE: string;
+ // 接口地址
+ VITE_GLOB_API_URL: string;
+ // 接口前缀
+ VITE_GLOB_API_URL_PREFIX?: string;
+ // Project abbreviation
+ VITE_GLOB_APP_SHORT_NAME: string;
+ // 图片上传地址
+ VITE_GLOB_UPLOAD_URL?: string;
+ //图片前缀地址
+ VITE_GLOB_IMG_URL?: string;
+}
diff --git a/web/types/global.d.ts b/web/types/global.d.ts
new file mode 100644
index 0000000..2506c61
--- /dev/null
+++ b/web/types/global.d.ts
@@ -0,0 +1,100 @@
+import type {
+ ComponentRenderProxy,
+ VNode,
+ VNodeChild,
+ ComponentPublicInstance,
+ FunctionalComponent,
+ PropType as VuePropType,
+} from 'vue';
+
+declare global {
+ const __APP_INFO__: {
+ pkg: {
+ name: string;
+ version: string;
+ dependencies: Recordable;
+ devDependencies: Recordable;
+ };
+ lastBuildTime: string;
+ };
+ // declare interface Window {
+ // // Global vue app instance
+ // __APP__: App;
+ // }
+
+ // vue
+ declare type PropType = VuePropType;
+ declare type VueNode = VNodeChild | JSX.Element;
+
+ export type Writable = {
+ -readonly [P in keyof T]: T[P];
+ };
+
+ declare type Nullable = T | null;
+ declare type NonNullable = T extends null | undefined ? never : T;
+ declare type Recordable = Record;
+ declare type ReadonlyRecordable = {
+ readonly [key: string]: T;
+ };
+ declare type Indexable = {
+ [key: string]: T;
+ };
+ declare type DeepPartial = {
+ [P in keyof T]?: DeepPartial;
+ };
+ declare type TimeoutHandle = ReturnType;
+ declare type IntervalHandle = ReturnType;
+
+ declare interface ChangeEvent extends Event {
+ target: HTMLInputElement;
+ }
+
+ declare interface WheelEvent {
+ path?: EventTarget[];
+ }
+
+ interface ImportMetaEnv extends ViteEnv {
+ __: unknown;
+ }
+
+ declare interface ViteEnv {
+ VITE_PORT: number;
+ VITE_PUBLIC_PATH: string;
+ VITE_GLOB_APP_TITLE: string;
+ VITE_GLOB_APP_SHORT_NAME: string;
+ VITE_DROP_CONSOLE: boolean;
+ VITE_GLOB_IMG_URL: string;
+ VITE_PROXY: [string, string][];
+ VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
+ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
+ }
+
+ declare function parseInt(s: string | number, radix?: number): number;
+
+ declare function parseFloat(string: string | number): number;
+
+ namespace JSX {
+ // tslint:disable no-empty-interface
+ type Element = VNode;
+ // tslint:disable no-empty-interface
+ type ElementClass = ComponentRenderProxy;
+
+ interface ElementAttributesProperty {
+ $props: any;
+ }
+
+ interface IntrinsicElements {
+ [elem: string]: any;
+ }
+
+ interface IntrinsicAttributes {
+ [elem: string]: any;
+ }
+ }
+}
+
+declare module 'vue' {
+ export type JSXComponent =
+ | { new (): ComponentPublicInstance }
+ | FunctionalComponent;
+}
diff --git a/web/types/index.d.ts b/web/types/index.d.ts
new file mode 100644
index 0000000..85d2492
--- /dev/null
+++ b/web/types/index.d.ts
@@ -0,0 +1,28 @@
+declare interface Fn {
+ (...arg: T[]): R;
+}
+
+declare interface PromiseFn {
+ (...arg: T[]): Promise;
+}
+
+declare type RefType = T | null;
+
+declare type LabelValueOptions = {
+ label: string;
+ value: any;
+ disabled: boolean;
+ [key: string]: string | number | boolean;
+}[];
+
+declare type EmitType = (event: string, ...args: any[]) => void;
+
+declare type TargetContext = '_self' | '_blank';
+
+declare interface ComponentElRef {
+ $el: T;
+}
+
+declare type ComponentRef = ComponentElRef | null;
+
+declare type ElRef = Nullable;
diff --git a/web/types/modules.d.ts b/web/types/modules.d.ts
new file mode 100644
index 0000000..b57b3cf
--- /dev/null
+++ b/web/types/modules.d.ts
@@ -0,0 +1,10 @@
+declare module '*.vue' {
+ import { DefineComponent } from 'vue';
+ const Component: DefineComponent<{}, {}, any>;
+ export default Component;
+}
+
+declare module 'virtual:*' {
+ const result: any;
+ export default result;
+}
diff --git a/web/types/utils.d.ts b/web/types/utils.d.ts
new file mode 100644
index 0000000..6500d44
--- /dev/null
+++ b/web/types/utils.d.ts
@@ -0,0 +1,5 @@
+import type { ComputedRef, Ref } from 'vue';
+
+export type DynamicProps = {
+ [P in keyof T]: Ref | T[P] | ComputedRef;
+};
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..831dcd1
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,83 @@
+import type { UserConfig, ConfigEnv } from 'vite';
+import { loadEnv } from 'vite';
+import { resolve } from 'path';
+import { wrapperEnv } from './build/utils';
+import { createVitePlugins } from './build/vite/plugin';
+import { OUTPUT_DIR } from './build/constant';
+import { createProxy } from './build/vite/proxy';
+import pkg from './package.json';
+import { format } from 'date-fns';
+const { dependencies, devDependencies, name, version } = pkg;
+
+const __APP_INFO__ = {
+ pkg: { dependencies, devDependencies, name, version },
+ lastBuildTime: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
+};
+
+function pathResolve(dir: string) {
+ return resolve(process.cwd(), '.', dir);
+}
+
+export default ({ command, mode }: ConfigEnv): UserConfig => {
+ const root = process.cwd();
+ const env = loadEnv(mode, root);
+ const viteEnv = wrapperEnv(env);
+ const { VITE_PUBLIC_PATH, VITE_PORT, VITE_PROXY } = viteEnv;
+ const isBuild = command === 'build';
+ return {
+ base: VITE_PUBLIC_PATH,
+ esbuild: {},
+ resolve: {
+ alias: [
+ {
+ find: /\/#\//,
+ replacement: pathResolve('types') + '/',
+ },
+ {
+ find: '@',
+ replacement: pathResolve('src') + '/',
+ },
+ ],
+ dedupe: ['vue'],
+ },
+ plugins: createVitePlugins(viteEnv, isBuild),
+ define: {
+ __APP_INFO__: JSON.stringify(__APP_INFO__),
+ __VUE_OPTIONS_API__: true,
+ __VUE_PROD_DEVTOOLS__: false,
+ __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
+ },
+ css: {
+ preprocessorOptions: {
+ less: {
+ modifyVars: {},
+ javascriptEnabled: true,
+ additionalData: `@import "src/styles/var.less";`,
+ },
+ },
+ },
+ server: {
+ host: true,
+ port: VITE_PORT,
+ proxy: createProxy(VITE_PROXY),
+ // proxy: {
+ // '/api': {
+ // target: '',
+ // changeOrigin: true,
+ // rewrite: (path) => path.replace(/^\/api/, '/api/v1')
+ // }
+ // }
+ },
+ optimizeDeps: {
+ include: [],
+ exclude: ['vue-demi'],
+ },
+ build: {
+ target: 'es2015',
+ cssTarget: 'chrome80',
+ outDir: OUTPUT_DIR,
+ reportCompressedSize: false,
+ chunkSizeWarningLimit: 3000,
+ },
+ };
+};