Merge branch 'refs/heads/1.1.0-beta3' into preview

This commit is contained in:
xlsea 2024-07-08 17:53:46 +08:00
commit 3ec4b5fe6c
11 changed files with 282 additions and 23 deletions

View File

@ -1,9 +1,9 @@
<script setup lang="tsx">
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 json from 'highlight.js/lib/languages/json';
import { ref } from 'vue';
import { ref, render } from 'vue';
import { taskStatusRecord } from '@/constants/business';
import { $t } from '@/locales';
import { parseArgsJson } from '@/utils/common';
@ -31,6 +31,8 @@ const emit = defineEmits<Emits>();
const expandedRowKeys = ref<DataTableRowKey[]>([]);
const argsDomMap = ref<Map<string, boolean>>(new Map());
const { columns, columnChecks, data, loading, mobilePagination } = useTable({
apiFn: fetchGetJobTaskList,
apiParams: {
@ -118,25 +120,52 @@ const { columns, columnChecks, data, loading, mobilePagination } = useTable({
titleAlign: 'center',
minWidth: 120,
render: row => {
const argsDom = () => (
<td class="n-data-table-td n-data-table-td--last-col" colspan={columns.value.length || 9}>
<NCode
class={`max-h-300px overflow-auto ${String(row.parentId) !== '0' ? 'pl-36px' : ''}`}
hljs={hljs}
code={parseArgsJson(row.argsStr)}
language="json"
show-line-numbers
/>
</td>
);
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 (
<NPopover trigger="click">
{{
trigger: () => (
<NButton type="primary" text>
<span class="w-28px ws-break-spaces">{`查看\n参数`}</span>
</NButton>
),
default: () => (
<NCode
class="max-h-300px overflow-auto"
hljs={hljs}
code={parseArgsJson(row.argsStr)}
language="json"
show-line-numbers
/>
)
}}
</NPopover>
<>
{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',
title: $t('page.jobBatch.jobTask.createDt'),
align: 'left',
minWidth: 120
minWidth: 130
}
]
});
@ -207,7 +236,7 @@ init();
:data="data"
:loading="loading"
remote
:scroll-x="962"
:scroll-x="1000"
:row-key="row => row.id"
:pagination="mobilePagination"
:indent="16"
@ -215,6 +244,7 @@ init();
allow-checking-not-loaded
:expanded-row-keys="expandedRowKeys"
class="sm:h-full"
:row-props="row => ({ id: `job-task-${row.id}` })"
@update:expanded-row-keys="onExpandedRowKeys"
@update:page="onUpdatePage"
@load="onLoad"

View File

@ -92,6 +92,10 @@ export function useTable<A extends NaiveUI.TableApiFn>(config: NaiveUI.NaiveTabl
} else if (column.type === 'expand') {
columnMap.set(EXPAND_KEY, column);
}
if (isMobile.value && column.fixed) {
column.fixed = undefined;
}
});
const filteredColumns = checks

View 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 };
}

View File

@ -10,6 +10,8 @@ defineOptions({
name: 'PageFun'
});
const isDev = import.meta.env.DEV;
const themeStore = useThemeStore();
const layoutMode = computed(() => themeStore.layout.mode);
@ -101,6 +103,12 @@ const isWrapperScrollMode = computed(() => themeStore.layout.scrollMode === 'wra
>
<NSwitch v-model:value="themeStore.footer.right" />
</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>
</template>

View File

@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
copySuccessMsg: 'Copy Success, Please replace the variable "themeSettings" in "src/theme/settings.ts"',
resetConfig: 'Reset Config',
resetSuccessMsg: 'Reset Success'
},
watermark: {
visible: 'Watermark Visible',
text: 'Watermark Text'
}
},
route: {

View File

@ -301,6 +301,10 @@ const local: App.I18n.Schema = {
copySuccessMsg: '复制成功,请替换 src/theme/settings.ts 中的变量 themeSettings',
resetConfig: '重置配置',
resetSuccessMsg: '重置成功'
},
watermark: {
visible: '开启',
text: '水印文字'
}
},
route: {

View File

@ -5,6 +5,7 @@ import { useEventListener, usePreferredColorScheme } from '@vueuse/core';
import { getPaletteColorByNumber } from '@sa/color';
import { SetupStoreId } from '@/enum';
import { localStg } from '@/utils/storage';
import { useWatermark } from '@/hooks/common/watermark';
import {
addThemeVarsToHtml,
createThemeToken,
@ -54,6 +55,26 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
*/
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 */
function resetStore() {
const themeStore = useThemeStore();
@ -171,6 +192,15 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
},
{ immediate: true }
);
watch(
settings.value.watermark,
val => {
toggleWatermark(val.visible);
setWatermarkText(val.text);
},
{ immediate: true }
);
});
/** On scope dispose */
@ -189,6 +219,8 @@ export const useThemeStore = defineStore(SetupStoreId.Theme, () => {
setThemeScheme,
toggleThemeScheme,
updateThemeColors,
setThemeLayout
setThemeLayout,
setWatermarkText,
toggleWatermark
};
});

View File

@ -46,6 +46,10 @@ export const themeSettings: App.Theme.ThemeSetting = {
fixed: false,
height: 48,
right: true
},
watermark: {
visible: true,
text: 'Snail Job'
}
};

View File

@ -1058,6 +1058,8 @@ declare namespace Api {
taskStatus: Common.TaskStatus;
/** 任务类型 */
taskType: Common.TaskType;
/** 父级 ID */
parentId: string;
/** 子节点 */
children: JobTaskTree[];
/** 是否存在下级 */

11
src/typings/app.d.ts vendored
View File

@ -97,6 +97,13 @@ declare namespace App {
/** Whether float the footer to the right when the layout is 'horizontal-mix' */
right: boolean;
};
/** Watermark */
watermark: {
/** Whether to show the watermark */
visible: boolean;
/** WatermarkText */
text: string;
};
}
interface OtherColor {
@ -521,6 +528,10 @@ declare namespace App {
resetConfig: string;
resetSuccessMsg: string;
};
watermark: {
visible: string;
text: string;
};
};
route: Record<I18nRouteKey, string>;
page: {

View File

@ -220,3 +220,33 @@ export function stringToContent(
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);
}
};
}