Merge branch 'refs/heads/1.1.0-beta3' into preview
This commit is contained in:
commit
3ec4b5fe6c
@ -1,9 +1,9 @@
|
|||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { DataTableRowKey } from 'naive-ui';
|
import type { DataTableRowKey } from 'naive-ui';
|
||||||
import { NButton, NCode, NPopover, NTag } from 'naive-ui';
|
import { NButton, NCode, NTag } from 'naive-ui';
|
||||||
import hljs from 'highlight.js/lib/core';
|
import hljs from 'highlight.js/lib/core';
|
||||||
import json from 'highlight.js/lib/languages/json';
|
import json from 'highlight.js/lib/languages/json';
|
||||||
import { ref } from 'vue';
|
import { ref, render } from 'vue';
|
||||||
import { taskStatusRecord } from '@/constants/business';
|
import { taskStatusRecord } from '@/constants/business';
|
||||||
import { $t } from '@/locales';
|
import { $t } from '@/locales';
|
||||||
import { parseArgsJson } from '@/utils/common';
|
import { parseArgsJson } from '@/utils/common';
|
||||||
@ -31,6 +31,8 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const expandedRowKeys = ref<DataTableRowKey[]>([]);
|
const expandedRowKeys = ref<DataTableRowKey[]>([]);
|
||||||
|
|
||||||
|
const argsDomMap = ref<Map<string, boolean>>(new Map());
|
||||||
|
|
||||||
const { columns, columnChecks, data, loading, mobilePagination } = useTable({
|
const { columns, columnChecks, data, loading, mobilePagination } = useTable({
|
||||||
apiFn: fetchGetJobTaskList,
|
apiFn: fetchGetJobTaskList,
|
||||||
apiParams: {
|
apiParams: {
|
||||||
@ -118,25 +120,52 @@ const { columns, columnChecks, data, loading, mobilePagination } = useTable({
|
|||||||
titleAlign: 'center',
|
titleAlign: 'center',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
render: row => {
|
render: row => {
|
||||||
return (
|
const argsDom = () => (
|
||||||
<NPopover trigger="click">
|
<td class="n-data-table-td n-data-table-td--last-col" colspan={columns.value.length || 9}>
|
||||||
{{
|
|
||||||
trigger: () => (
|
|
||||||
<NButton type="primary" text>
|
|
||||||
<span class="w-28px ws-break-spaces">{`查看\n参数`}</span>
|
|
||||||
</NButton>
|
|
||||||
),
|
|
||||||
default: () => (
|
|
||||||
<NCode
|
<NCode
|
||||||
class="max-h-300px overflow-auto"
|
class={`max-h-300px overflow-auto ${String(row.parentId) !== '0' ? 'pl-36px' : ''}`}
|
||||||
hljs={hljs}
|
hljs={hljs}
|
||||||
code={parseArgsJson(row.argsStr)}
|
code={parseArgsJson(row.argsStr)}
|
||||||
language="json"
|
language="json"
|
||||||
show-line-numbers
|
show-line-numbers
|
||||||
/>
|
/>
|
||||||
)
|
</td>
|
||||||
}}
|
);
|
||||||
</NPopover>
|
|
||||||
|
const handleView = () => {
|
||||||
|
if (argsDomMap.value.get(row.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tr = document.querySelector(`#job-task-${row.id}`);
|
||||||
|
const argsTr = document.createElement('tr');
|
||||||
|
argsTr.setAttribute('id', `job-task-args-${row.id}`);
|
||||||
|
argsTr.setAttribute('class', 'n-data-table-tr n-data-table-tr--expanded');
|
||||||
|
tr?.after(argsTr);
|
||||||
|
render(argsDom(), argsTr!);
|
||||||
|
argsDomMap.value.set(row.id, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!argsDomMap.value.get(row.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tr = document.querySelector(`#job-task-args-${row.id}`);
|
||||||
|
tr?.remove();
|
||||||
|
argsDomMap.value.set(row.id, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{argsDomMap.value.get(row.id) ? (
|
||||||
|
<NButton type="primary" text onClick={() => handleClose()}>
|
||||||
|
<span class="w-28px ws-break-spaces">{`收起`}</span>
|
||||||
|
</NButton>
|
||||||
|
) : (
|
||||||
|
<NButton type="primary" text onClick={() => handleView()}>
|
||||||
|
<span class="w-28px ws-break-spaces">{`查看\n参数`}</span>
|
||||||
|
</NButton>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -156,7 +185,7 @@ const { columns, columnChecks, data, loading, mobilePagination } = useTable({
|
|||||||
key: 'createDt',
|
key: 'createDt',
|
||||||
title: $t('page.jobBatch.jobTask.createDt'),
|
title: $t('page.jobBatch.jobTask.createDt'),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
minWidth: 120
|
minWidth: 130
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -207,7 +236,7 @@ init();
|
|||||||
:data="data"
|
:data="data"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
remote
|
remote
|
||||||
:scroll-x="962"
|
:scroll-x="1000"
|
||||||
:row-key="row => row.id"
|
:row-key="row => row.id"
|
||||||
:pagination="mobilePagination"
|
:pagination="mobilePagination"
|
||||||
:indent="16"
|
:indent="16"
|
||||||
@ -215,6 +244,7 @@ init();
|
|||||||
allow-checking-not-loaded
|
allow-checking-not-loaded
|
||||||
:expanded-row-keys="expandedRowKeys"
|
:expanded-row-keys="expandedRowKeys"
|
||||||
class="sm:h-full"
|
class="sm:h-full"
|
||||||
|
:row-props="row => ({ id: `job-task-${row.id}` })"
|
||||||
@update:expanded-row-keys="onExpandedRowKeys"
|
@update:expanded-row-keys="onExpandedRowKeys"
|
||||||
@update:page="onUpdatePage"
|
@update:page="onUpdatePage"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
|
@ -92,6 +92,10 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
|
|||||||
} else if (column.type === 'expand') {
|
} else if (column.type === 'expand') {
|
||||||
columnMap.set(EXPAND_KEY, column);
|
columnMap.set(EXPAND_KEY, column);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMobile.value && column.fixed) {
|
||||||
|
column.fixed = undefined;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredColumns = checks
|
const filteredColumns = checks
|
||||||
|
130
src/hooks/common/watermark.ts
Normal file
130
src/hooks/common/watermark.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { Ref } from 'vue';
|
||||||
|
import { ref, shallowRef, unref } from 'vue';
|
||||||
|
import { debounce } from '@/utils/common';
|
||||||
|
|
||||||
|
type Attrs = {
|
||||||
|
textStyles?: {
|
||||||
|
font?: string;
|
||||||
|
fillStyle?: string;
|
||||||
|
rotate?: number;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
styles?: { [key: string]: any };
|
||||||
|
};
|
||||||
|
|
||||||
|
type WatermarkOpts = {
|
||||||
|
appendEl?: Ref<HTMLElement | null>;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useWatermark(opts: WatermarkOpts = {}) {
|
||||||
|
// const id = `waterMark_${Math.random().toString().slice(-10)}_${+new Date()}`
|
||||||
|
const appendEl = opts.appendEl || (ref(document.body) as Ref<HTMLElement>);
|
||||||
|
const watermarkEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
|
/** 绘制canvas文字背景图 */
|
||||||
|
const createCanvasBase64 = (str: string, attrs: Attrs = {}) => {
|
||||||
|
const can = document.createElement('canvas');
|
||||||
|
const { rotate, font, fillStyle, width = 200, height = 140 } = attrs.textStyles || {};
|
||||||
|
Object.assign(can, { width, height });
|
||||||
|
const cans = can.getContext('2d');
|
||||||
|
if (cans) {
|
||||||
|
cans.rotate((-(rotate ?? 20) * Math.PI) / 180);
|
||||||
|
cans.font = font || '12px Vedana';
|
||||||
|
cans.fillStyle = fillStyle || 'rgba(200, 200, 200, 0.3)';
|
||||||
|
cans.textAlign = 'left';
|
||||||
|
cans.textBaseline = 'middle';
|
||||||
|
cans.fillText(str, can.width / 10, can.height / 2);
|
||||||
|
}
|
||||||
|
return can.toDataURL('image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 页面随窗口调整更新水印 */
|
||||||
|
const updateWatermark = (
|
||||||
|
watermarkOpts: {
|
||||||
|
str?: string;
|
||||||
|
attrs?: Attrs;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
|
const el = unref(watermarkEl);
|
||||||
|
if (!el) return;
|
||||||
|
if (typeof watermarkOpts.width !== 'undefined') {
|
||||||
|
el.style.width = `${watermarkOpts.width}px`;
|
||||||
|
}
|
||||||
|
if (typeof watermarkOpts.height !== 'undefined') {
|
||||||
|
el.style.height = `${watermarkOpts.height}px`;
|
||||||
|
}
|
||||||
|
if (typeof watermarkOpts.str !== 'undefined') {
|
||||||
|
el.style.background = `url(${createCanvasBase64(watermarkOpts.str, watermarkOpts.attrs)}) left top repeat`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 绘制水印层 */
|
||||||
|
const createWatermark = (str: string, attrs: Attrs = {}) => {
|
||||||
|
if (watermarkEl.value) {
|
||||||
|
updateWatermark({ str, attrs });
|
||||||
|
return watermarkEl;
|
||||||
|
}
|
||||||
|
const div = document.createElement('div');
|
||||||
|
watermarkEl.value = div;
|
||||||
|
if (opts.id) {
|
||||||
|
const last_el = document.getElementById(opts.id);
|
||||||
|
if (last_el) {
|
||||||
|
document.body.removeChild(last_el);
|
||||||
|
}
|
||||||
|
div.id = opts.id;
|
||||||
|
}
|
||||||
|
Object.assign(
|
||||||
|
div.style,
|
||||||
|
{
|
||||||
|
pointerEvents: 'none',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: '100000'
|
||||||
|
},
|
||||||
|
attrs.styles || {}
|
||||||
|
);
|
||||||
|
const el = unref(appendEl);
|
||||||
|
if (!el) return watermarkEl;
|
||||||
|
const { clientHeight: height, clientWidth: width } = el;
|
||||||
|
updateWatermark({ str, attrs, width, height });
|
||||||
|
el.appendChild(div);
|
||||||
|
return watermarkEl;
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceUpdateResize = debounce(
|
||||||
|
() => {
|
||||||
|
const el = unref(appendEl);
|
||||||
|
if (!el) return;
|
||||||
|
const { clientHeight: height, clientWidth: width } = el;
|
||||||
|
updateWatermark({ width, height });
|
||||||
|
},
|
||||||
|
30,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 对外提供的设置水印方法 */
|
||||||
|
const setWatermark = (str: string, attrs: Attrs = {}) => {
|
||||||
|
createWatermark(str, attrs);
|
||||||
|
window.addEventListener('resize', debounceUpdateResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 清除水印 */
|
||||||
|
const clearWatermark = () => {
|
||||||
|
let domId: HTMLElement | null | undefined = unref(watermarkEl);
|
||||||
|
if (!domId && opts.id) {
|
||||||
|
domId = document.getElementById(opts.id);
|
||||||
|
}
|
||||||
|
watermarkEl.value = undefined;
|
||||||
|
const el = unref(appendEl);
|
||||||
|
if (!el) return;
|
||||||
|
domId && el.removeChild(domId);
|
||||||
|
window.removeEventListener('resize', debounceUpdateResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { setWatermark, clearWatermark };
|
||||||
|
}
|
@ -10,6 +10,8 @@ defineOptions({
|
|||||||
name: 'PageFun'
|
name: 'PageFun'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isDev = import.meta.env.DEV;
|
||||||
|
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
const layoutMode = computed(() => themeStore.layout.mode);
|
const layoutMode = computed(() => themeStore.layout.mode);
|
||||||
@ -101,6 +103,12 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
|
|||||||
>
|
>
|
||||||
<NSwitch v-model:value="themeStore.footer.right" />
|
<NSwitch v-model:value="themeStore.footer.right" />
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isDev" key="8" :label="$t('theme.watermark.visible')">
|
||||||
|
<NSwitch v-model:value="themeStore.watermark.visible" />
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem v-if="isDev" key="8-1" :label="$t('theme.watermark.text')">
|
||||||
|
<NInput v-model:value="themeStore.watermark.text" size="small" :step="1" class="max-w-180px" />
|
||||||
|
</SettingItem>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
|
|||||||
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
|
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
|
||||||
resetConfig: 'Reset Config',
|
resetConfig: 'Reset Config',
|
||||||
resetSuccessMsg: 'Reset Success'
|
resetSuccessMsg: 'Reset Success'
|
||||||
|
},
|
||||||
|
watermark: {
|
||||||
|
visible: 'Watermark Visible',
|
||||||
|
text: 'Watermark Text'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
route: {
|
route: {
|
||||||
|
@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
|
|||||||
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
|
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
|
||||||
resetConfig: '重置配置',
|
resetConfig: '重置配置',
|
||||||
resetSuccessMsg: '重置成功'
|
resetSuccessMsg: '重置成功'
|
||||||
|
},
|
||||||
|
watermark: {
|
||||||
|
visible: '开启',
|
||||||
|
text: '水印文字'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
route: {
|
route: {
|
||||||
|
@ -5,6 +5,7 @@ import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
|
|||||||
import { getPaletteColorByNumber } from '@sa/color';
|
import { getPaletteColorByNumber } from '@sa/color';
|
||||||
import { SetupStoreId } from '@/enum';
|
import { SetupStoreId } from '@/enum';
|
||||||
import { localStg } from '@/utils/storage';
|
import { localStg } from '@/utils/storage';
|
||||||
|
import { useWatermark } from '@/hooks/common/watermark';
|
||||||
import {
|
import {
|
||||||
addThemeVarsToHtml,
|
addThemeVarsToHtml,
|
||||||
createThemeToken,
|
createThemeToken,
|
||||||
@ -54,6 +55,26 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
*/
|
*/
|
||||||
const settingsJson = computed(() => JSON.stringify(settings.value));
|
const settingsJson = computed(() => JSON.stringify(settings.value));
|
||||||
|
|
||||||
|
/** Watermarks */
|
||||||
|
const { setWatermark, clearWatermark } = useWatermark({ id: 'global_watermark_id' });
|
||||||
|
|
||||||
|
/** 开启水印 */
|
||||||
|
function toggleWatermark(visible: boolean) {
|
||||||
|
visible ? setWatermark(settings.value.watermark.text) : clearWatermark();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改水印文案 */
|
||||||
|
function setWatermarkText(text: string) {
|
||||||
|
if (!text) {
|
||||||
|
clearWatermark();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (settings.value.watermark.visible) {
|
||||||
|
settings.value.watermark.text = text;
|
||||||
|
setWatermark(settings.value.watermark.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Reset store */
|
/** Reset store */
|
||||||
function resetStore() {
|
function resetStore() {
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
@ -171,6 +192,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
settings.value.watermark,
|
||||||
|
val => {
|
||||||
|
toggleWatermark(val.visible);
|
||||||
|
setWatermarkText(val.text);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** On scope dispose */
|
/** On scope dispose */
|
||||||
@ -189,6 +219,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
|
|||||||
setThemeScheme,
|
setThemeScheme,
|
||||||
toggleThemeScheme,
|
toggleThemeScheme,
|
||||||
updateThemeColors,
|
updateThemeColors,
|
||||||
setThemeLayout
|
setThemeLayout,
|
||||||
|
setWatermarkText,
|
||||||
|
toggleWatermark
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -46,6 +46,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
|
|||||||
fixed: false,
|
fixed: false,
|
||||||
height: 48,
|
height: 48,
|
||||||
right: true
|
right: true
|
||||||
|
},
|
||||||
|
watermark: {
|
||||||
|
visible: true,
|
||||||
|
text: 'Snail Job'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
2
src/typings/api.d.ts
vendored
2
src/typings/api.d.ts
vendored
@ -1058,6 +1058,8 @@ declare namespace Api {
|
|||||||
taskStatus: Common.TaskStatus;
|
taskStatus: Common.TaskStatus;
|
||||||
/** 任务类型 */
|
/** 任务类型 */
|
||||||
taskType: Common.TaskType;
|
taskType: Common.TaskType;
|
||||||
|
/** 父级 ID */
|
||||||
|
parentId: string;
|
||||||
/** 子节点 */
|
/** 子节点 */
|
||||||
children: JobTaskTree[];
|
children: JobTaskTree[];
|
||||||
/** 是否存在下级 */
|
/** 是否存在下级 */
|
||||||
|
11
src/typings/app.d.ts
vendored
11
src/typings/app.d.ts
vendored
@ -97,6 +97,13 @@ declare namespace App {
|
|||||||
/** Whether float the footer to the right when the layout is 'horizontal-mix' */
|
/** Whether float the footer to the right when the layout is 'horizontal-mix' */
|
||||||
right: boolean;
|
right: boolean;
|
||||||
};
|
};
|
||||||
|
/** Watermark */
|
||||||
|
watermark: {
|
||||||
|
/** Whether to show the watermark */
|
||||||
|
visible: boolean;
|
||||||
|
/** WatermarkText */
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OtherColor {
|
interface OtherColor {
|
||||||
@ -521,6 +528,10 @@ declare namespace App {
|
|||||||
resetConfig: string;
|
resetConfig: string;
|
||||||
resetSuccessMsg: string;
|
resetSuccessMsg: string;
|
||||||
};
|
};
|
||||||
|
watermark: {
|
||||||
|
visible: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
route: Record<I18nRouteKey, string>;
|
route: Record<I18nRouteKey, string>;
|
||||||
page: {
|
page: {
|
||||||
|
@ -220,3 +220,33 @@ export function stringToContent(
|
|||||||
|
|
||||||
return result as any;
|
return result as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数防抖
|
||||||
|
*
|
||||||
|
* @param func 函数
|
||||||
|
* @param wait 延迟毫秒数
|
||||||
|
* @param immediate true/false (是/否)即执行
|
||||||
|
*/
|
||||||
|
export function debounce(func: () => any, wait: number, immediate?: boolean) {
|
||||||
|
let timeout: any;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-ignore
|
||||||
|
const context = this
|
||||||
|
const args: any = arguments
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
if (immediate) {
|
||||||
|
const callNow = !timeout;
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
}, wait);
|
||||||
|
if (callNow) func.apply(context, args);
|
||||||
|
} else {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
func.apply(context, args);
|
||||||
|
}, wait);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user