new-map
ihzero 2022-11-16 20:42:41 +08:00
parent 94ab292232
commit 6be280c6e1
11 changed files with 816 additions and 35 deletions

26
src/api/upload.ts 100644
View File

@ -0,0 +1,26 @@
import { defHttp } from '/@/utils/http/axios'
import { UploadFileParams } from '/#/axios'
import { useGlobSetting } from '/@/hooks/setting'
const { uploadUrl = '' } = useGlobSetting()
interface UploadApiResult {
message: string
code: number
data: string
}
/**
* @description: Upload interface
*/
export function uploadApi(
params: UploadFileParams,
onUploadProgress: (progressEvent: ProgressEvent) => void,
) {
return defHttp.uploadFile<UploadApiResult>(
{
url: uploadUrl + '/api/web/upload',
onUploadProgress,
},
params,
)
}

View File

@ -0,0 +1,4 @@
import { withInstall } from '/@/utils/index';
import tinymce from './src/Editor.vue';
export const Tinymce = withInstall(tinymce);

View File

@ -0,0 +1,346 @@
<template>
<div :class="prefixCls" :style="{ width: containerWidth }">
<ImgUpload
:fullscreen="fullscreen"
@uploading="handleImageUploading"
@done="handleDone"
v-if="showImageUpload"
v-show="editorRef"
:disabled="disabled"
/>
<textarea
:id="tinymceId"
ref="elRef"
:style="{ visibility: 'hidden' }"
v-if="!initOptions.inline"
></textarea>
<slot v-else></slot>
</div>
</template>
<script lang="ts">
import type { Editor, RawEditorSettings } from 'tinymce';
import tinymce from 'tinymce/tinymce';
import 'tinymce/themes/silver';
import 'tinymce/icons/default/icons';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/code';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/insertdatetime';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/media';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/noneditable';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/print';
import 'tinymce/plugins/save';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/spellchecker';
import 'tinymce/plugins/tabfocus';
// import 'tinymce/plugins/table';
import 'tinymce/plugins/template';
import 'tinymce/plugins/textpattern';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
import {
defineComponent,
computed,
nextTick,
ref,
unref,
watch,
onDeactivated,
onBeforeUnmount,
} from 'vue';
import ImgUpload from './ImgUpload.vue';
import { toolbar, plugins } from './tinymce';
import { buildShortUUID } from '/@/utils/uuid';
import { bindHandlers } from './helper';
import { onMountedOrActivated } from '/@/hooks/core/onMountedOrActivated';
import { useDesign } from '/@/hooks/web/useDesign';
import { isNumber } from '/@/utils/is';
import { useLocale } from '/@/locales/useLocale';
import { useAppStore } from '/@/store/modules/app';
const tinymceProps = {
options: {
type: Object as PropType<Partial<RawEditorSettings>>,
default: () => ({}),
},
value: {
type: String,
},
toolbar: {
type: Array as PropType<string[]>,
default: toolbar,
},
plugins: {
type: Array as PropType<string[]>,
default: plugins,
},
modelValue: {
type: String,
},
height: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 400,
},
width: {
type: [Number, String] as PropType<string | number>,
required: false,
default: 'auto',
},
showImageUpload: {
type: Boolean,
default: true,
},
};
export default defineComponent({
name: 'Tinymce',
components: { ImgUpload },
inheritAttrs: false,
props: tinymceProps,
emits: ['change', 'update:modelValue', 'inited', 'init-error'],
setup(props, { emit, attrs }) {
const editorRef = ref<Nullable<Editor>>(null);
const fullscreen = ref(false);
const tinymceId = ref<string>(buildShortUUID('tiny-vue'));
const elRef = ref<Nullable<HTMLElement>>(null);
const { prefixCls } = useDesign('tinymce-container');
const appStore = useAppStore();
const tinymceContent = computed(() => props.modelValue);
const containerWidth = computed(() => {
const width = props.width;
if (isNumber(width)) {
return `${width}px`;
}
return width;
});
const skinName = computed(() => {
return appStore.getDarkMode === 'light' ? 'oxide' : 'oxide-dark';
});
const langName = computed(() => {
const lang = useLocale().getLocale.value;
return ['zh_CN', 'en'].includes(lang) ? lang : 'zh_CN';
});
const initOptions = computed((): RawEditorSettings => {
const { height, options, toolbar, plugins } = props;
const publicPath = import.meta.env.VITE_PUBLIC_PATH || '/';
return {
selector: `#${unref(tinymceId)}`,
height,
toolbar,
menubar: 'file edit insert view format table',
plugins,
language_url: publicPath + 'resource/tinymce/langs/' + langName.value + '.js',
language: langName.value,
branding: false,
default_link_target: '_blank',
link_title: false,
object_resizing: false,
auto_focus: true,
skin: skinName.value,
skin_url: publicPath + 'resource/tinymce/skins/ui/' + skinName.value,
content_css:
publicPath + 'resource/tinymce/skins/ui/' + skinName.value + '/content.min.css',
...options,
setup: (editor: Editor) => {
editorRef.value = editor;
editor.on('init', (e) => initSetup(e));
},
};
});
const disabled = computed(() => {
const { options } = props;
const getdDisabled = options && Reflect.get(options, 'readonly');
const editor = unref(editorRef);
if (editor) {
editor.setMode(getdDisabled ? 'readonly' : 'design');
}
return getdDisabled ?? false;
});
watch(
() => attrs.disabled,
() => {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.setMode(attrs.disabled ? 'readonly' : 'design');
},
);
onMountedOrActivated(() => {
if (!initOptions.value.inline) {
tinymceId.value = buildShortUUID('tiny-vue');
}
nextTick(() => {
setTimeout(() => {
initEditor();
}, 30);
});
});
onBeforeUnmount(() => {
destory();
});
onDeactivated(() => {
destory();
});
function destory() {
if (tinymce !== null) {
tinymce?.remove?.(unref(initOptions).selector!);
}
}
function initEditor() {
const el = unref(elRef);
if (el) {
el.style.visibility = '';
}
tinymce
.init(unref(initOptions))
.then((editor) => {
emit('inited', editor);
})
.catch((err) => {
emit('init-error', err);
});
}
function initSetup(e) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const value = props.modelValue || '';
editor.setContent(value);
bindModelHandlers(editor);
bindHandlers(e, attrs, unref(editorRef));
}
function setValue(editor: Recordable, val: string, prevVal?: string) {
if (
editor &&
typeof val === 'string' &&
val !== prevVal &&
val !== editor.getContent({ format: attrs.outputFormat })
) {
editor.setContent(val);
}
}
function bindModelHandlers(editor: any) {
const modelEvents = attrs.modelEvents ? attrs.modelEvents : null;
const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents;
watch(
() => props.modelValue,
(val: string, prevVal: string) => {
setValue(editor, val, prevVal);
},
);
watch(
() => props.value,
(val: string, prevVal: string) => {
setValue(editor, val, prevVal);
},
{
immediate: true,
},
);
editor.on(normalizedEvents ? normalizedEvents : 'change keyup undo redo', () => {
const content = editor.getContent({ format: attrs.outputFormat });
emit('update:modelValue', content);
emit('change', content);
});
editor.on('FullscreenStateChanged', (e) => {
fullscreen.value = e.state;
});
}
function handleImageUploading(name: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
editor.execCommand('mceInsertContent', false, getUploadingImgName(name));
const content = editor?.getContent() ?? '';
setValue(editor, content);
}
function handleDone(name: string, url: string) {
const editor = unref(editorRef);
if (!editor) {
return;
}
const content = editor?.getContent() ?? '';
const val = content?.replace(getUploadingImgName(name), `<img src="${url}"/>`) ?? '';
setValue(editor, val);
}
function getUploadingImgName(name: string) {
return `[uploading:${name}]`;
}
return {
prefixCls,
containerWidth,
initOptions,
tinymceContent,
elRef,
tinymceId,
handleImageUploading,
handleDone,
editorRef,
fullscreen,
disabled,
};
},
});
</script>
<style lang="less" scoped></style>
<style lang="less">
@prefix-cls: ~'@{namespace}-tinymce-container';
.@{prefix-cls} {
position: relative;
line-height: normal;
textarea {
z-index: -1;
visibility: hidden;
}
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div :class="[prefixCls, { fullscreen }]">
<Upload
name="file"
multiple
@change="handleChange"
:action="uploadUrl"
:showUploadList="false"
accept=".jpg,.jpeg,.gif,.png,.webp"
>
<a-button type="primary" v-bind="{ ...getButtonProps }">
{{ t('component.upload.imgUpload') }}
</a-button>
</Upload>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { Upload } from 'ant-design-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useGlobSetting } from '/@/hooks/setting';
import { useI18n } from '/@/hooks/web/useI18n';
export default defineComponent({
name: 'TinymceImageUpload',
components: { Upload },
props: {
fullscreen: {
type: Boolean,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['uploading', 'done', 'error'],
setup(props, { emit }) {
let uploading = false;
const { uploadUrl } = useGlobSetting();
const { t } = useI18n();
const { prefixCls } = useDesign('tinymce-img-upload');
const getButtonProps = computed(() => {
const { disabled } = props;
return {
disabled,
};
});
function handleChange(info: Recordable) {
const file = info.file;
const status = file?.status;
const url = file?.response?.url;
const name = file?.name;
if (status === 'uploading') {
if (!uploading) {
emit('uploading', name);
uploading = true;
}
} else if (status === 'done') {
emit('done', name, url);
uploading = false;
} else if (status === 'error') {
emit('error');
uploading = false;
}
}
return {
prefixCls,
handleChange,
uploadUrl,
t,
getButtonProps,
};
},
});
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-tinymce-img-upload';
.@{prefix-cls} {
position: absolute;
top: 4px;
right: 10px;
z-index: 20;
&.fullscreen {
position: fixed;
z-index: 10000;
}
}
</style>

View File

@ -0,0 +1,81 @@
const validEvents = [
'onActivate',
'onAddUndo',
'onBeforeAddUndo',
'onBeforeExecCommand',
'onBeforeGetContent',
'onBeforeRenderUI',
'onBeforeSetContent',
'onBeforePaste',
'onBlur',
'onChange',
'onClearUndos',
'onClick',
'onContextMenu',
'onCopy',
'onCut',
'onDblclick',
'onDeactivate',
'onDirty',
'onDrag',
'onDragDrop',
'onDragEnd',
'onDragGesture',
'onDragOver',
'onDrop',
'onExecCommand',
'onFocus',
'onFocusIn',
'onFocusOut',
'onGetContent',
'onHide',
'onInit',
'onKeyDown',
'onKeyPress',
'onKeyUp',
'onLoadContent',
'onMouseDown',
'onMouseEnter',
'onMouseLeave',
'onMouseMove',
'onMouseOut',
'onMouseOver',
'onMouseUp',
'onNodeChange',
'onObjectResizeStart',
'onObjectResized',
'onObjectSelected',
'onPaste',
'onPostProcess',
'onPostRender',
'onPreProcess',
'onProgressState',
'onRedo',
'onRemove',
'onReset',
'onSaveContent',
'onSelectionChange',
'onSetAttrib',
'onSetContent',
'onShow',
'onSubmit',
'onUndo',
'onVisualAid',
];
const isValidKey = (key: string) => validEvents.indexOf(key) !== -1;
export const bindHandlers = (initEvent: Event, listeners: any, editor: any): void => {
Object.keys(listeners)
.filter(isValidKey)
.forEach((key: string) => {
const handler = listeners[key];
if (typeof handler === 'function') {
if (key === 'onInit') {
handler(initEvent, editor);
} else {
editor.on(key.substring(2), (e: any) => handler(e, editor));
}
}
});
};

View File

@ -0,0 +1,13 @@
// Any plugins you want to setting has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration
export const plugins = [
'advlist anchor autolink autosave code codesample directionality fullscreen hr insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus template textpattern visualblocks visualchars wordcount',
];
export const toolbar = [
'fontsizeselect lineheight searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample',
'hr bullist numlist link preview anchor pagebreak insertdatetime media forecolor backcolor fullscreen',
];

View File

@ -192,6 +192,7 @@
item.percent = complete
},
)
item.status = UploadResultStatus.SUCCESS
item.responseData = data
return {
@ -249,7 +250,9 @@
for (const item of fileListRef.value) {
const { status, responseData } = item
if (status === UploadResultStatus.SUCCESS && responseData) {
fileList.push(responseData.url)
console.log(responseData?.data?.file)
fileList.push(responseData?.data?.file)
}
}
//

View File

@ -22,9 +22,9 @@ html {
src: url('../assets/fonts/pmzd.TTF');
}
.ant-modal-wrap {
z-index: 9999999 !important;
}
// .ant-modal-wrap {
// z-index: 9999999 !important;
// }
html,
body {

View File

@ -0,0 +1,82 @@
<template>
<BasicModal
v-bind="$attrs"
@register="registerModal"
showFooter
:title="getTitle"
width="980px"
@ok="handleSubmit"
>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue'
import { BasicForm, useForm } from '/@/components/Form/index'
import { accountFormSchema } from './links.data'
import { BasicModal, useModalInner } from '/@/components/Modal'
import { defaultsDeep } from 'lodash-es'
import { createDevice, getDeviceTypes, updateDevice } from '/@/api/sys/other'
const isUpdate = ref(false)
const getTitle = computed(() => (!isUpdate.value ? '新增链接' : '编辑链接'))
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
labelWidth: 60,
baseColProps: { span: 24 },
schemas: accountFormSchema,
showActionButtonGroup: false,
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = data?.isUpdate
if (unref(isUpdate)) {
const obj = Object.assign({}, { ...data, ...data?.extends })
const deviceTypes = await getDeviceTypes()
await setFieldsValue({
...obj,
agricultural_base_id: obj.base_id,
type: formatDataByObject(deviceTypes).find((e) => e.label == obj.type)?.value,
})
}
})
const formatDataByObject = (obj) => {
const arr: any[] = []
Object.keys(obj).forEach((e) => {
arr.push({
label: obj[e],
value: e,
})
})
return arr
}
const setValue = (keys: any, value: any) => {
const object = {}
var last = keys.pop()
keys.reduce((o, k) => (o[k] = o[k] || {}), object)[last] = value
return object
}
const handleSubmit = async () => {
const values = await validate()
console.log(values)
// try {
// const values = await validate()
// let params = {}
// for (const key in values) {
// params = defaultsDeep({}, params, setValue(key.split('.'), values[key]))
// }
// setModalProps({ confirmLoading: true })
// if (values.id) {
// //
// await updateDevice(values.id, params)
// } else {
// //
// await createDevice(params)
// }
// closeModal()
// emits('success')
// } finally {
// setModalProps({ confirmLoading: false })
// }
}
</script>

View File

@ -1,5 +1,13 @@
<template>
<div> <BasicTable @register="registerTable" /> </div>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button type="primary" @click="handleCreate"> </a-button>
</template>
</BasicTable>
<LinksDrawer @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts">
@ -7,13 +15,17 @@
import { reactive, toRefs } from 'vue'
import { getFriendinks } from '/@/api/sys/user'
import { columns, searchFormSchema } from './links.data'
import { useModal } from '/@/components/Modal'
import LinksDrawer from './LinksDrawer.vue'
import { message } from 'ant-design-vue'
export default {
components: {
BasicTable,
LinksDrawer,
},
setup() {
const state = reactive({})
const [registerModal, { openModal }] = useModal()
const [registerTable, { reload }] = useTable({
title: '账号列表',
api: async (e) => {
@ -26,7 +38,7 @@
rowKey: 'id',
columns,
formConfig: {
labelWidth: 120,
labelWidth: 80,
schemas: searchFormSchema,
},
useSearchForm: true,
@ -35,9 +47,23 @@
showIndexColumn: true,
})
const handleSuccess = () => {
message.success('操作成功')
reload()
}
const handleCreate = () => {
openModal(true, {
id: false,
})
}
return {
registerTable,
registerModal,
handleSuccess,
...toRefs(state),
handleCreate,
}
},
}

View File

@ -6,7 +6,36 @@ import { Tag } from 'ant-design-vue'
import { Switch } from 'ant-design-vue'
import { useMessage } from '/@/hooks/web/useMessage'
import { updateFriendinks } from '/@/api/sys/user'
import { ColEx } from '/@/components/Form/src/types'
import { Tinymce } from '/@/components/Tinymce/index'
import { uploadApi } from '/@/api/upload'
const colProps: Partial<ColEx> = {
xs: 24,
sm: 12,
md: 8,
lg: 6,
xl: 6,
xxl: 4,
}
const options = [
{
value: 1,
color: 'red',
label: '链接',
},
{
value: 2,
color: 'green',
label: '视频',
},
{
value: 3,
color: 'pink',
label: '文章',
},
]
export const columns: BasicColumn[] = [
{
title: '名称',
@ -20,23 +49,7 @@ export const columns: BasicColumn[] = [
customRender: ({ record }) => {
const status = record.type
const list = [
{
value: 1,
color: 'red',
label: '链接',
},
{
value: 2,
color: 'green',
label: '视频',
},
{
value: 3,
color: 'pink',
label: '文章',
},
]
const list = options
const item = list.find((e) => e.value === status)
const color = item?.color ?? 'red'
const text = item?.label ?? status
@ -62,26 +75,56 @@ export const columns: BasicColumn[] = [
const { createMessage } = useMessage()
updateFriendinks({
id: record.id,
is_recommend: !checked,
is_recommend: checked,
})
.then(() => {
record.is_recommend = newStatus
createMessage.success(`已成功设置推荐`)
createMessage.success(`已成功设置推荐状态`)
})
.catch(() => {
createMessage.error('修改推荐失败')
})
.finally(() => {
record.pendingRecommendStatusx = false
record.pendingRecommendStatus = false
})
},
})
},
},
{
title: '开启',
title: '状态',
dataIndex: 'is_show',
width: 180,
customRender: ({ record }) => {
if (!Reflect.has(record, 'pendingShowStatus')) {
record.pendingShowStatus = false
}
return h(Switch, {
checked: record.is_show === 1,
checkedChildren: '开启',
unCheckedChildren: '关闭',
loading: record.pendingShowStatus,
onChange(checked: boolean) {
record.pendingShowStatus = true
const newStatus = checked ? 1 : 0
const { createMessage } = useMessage()
updateFriendinks({
id: record.id,
is_show: checked,
})
.then(() => {
record.is_show = newStatus
createMessage.success(`已成功设置推荐状态`)
})
.catch(() => {
createMessage.error('修改推荐失败')
})
.finally(() => {
record.pendingShowStatus = false
})
},
})
},
},
{
title: '创建时间',
@ -100,22 +143,83 @@ export const searchFormSchema: FormSchema[] = [
label: '动作',
component: 'Select',
componentProps: {
options: [
{ label: 'create', value: 'create' },
{ label: 'update', value: 'update' },
{ label: 'delete', value: 'delete' },
],
options,
},
colProps: { span: 6 },
colProps,
},
{
field: '[start_time, end_time]',
label: '时间范围',
component: 'RangePicker',
componentProps: {
class: 'w-full',
format: 'YYYY-MM-DD',
placeholder: ['开始时间', '结束时间'],
},
colProps: { span: 6 },
colProps,
},
]
export const accountFormSchema: FormSchema[] = [
{
field: 'id',
label: 'ID',
required: false,
dynamicDisabled: true,
component: 'Input',
ifShow: ({ values }) => {
return !!values.id
},
},
{
field: 'type',
label: '类型',
required: true,
component: 'Select',
defaultValue: 1,
componentProps: {
options,
},
},
{
field: 'content',
label: '内容',
required: true,
component: 'Input',
ifShow: ({ values }) => {
return values.type == 3
},
render: ({ model, field }) => {
return h(Tinymce, {
value: model[field],
onChange: (value: string) => {
model[field] = value
},
})
},
},
{
field: 'content',
label: '地址',
required: true,
component: 'Upload',
componentProps: {
maxSize: 10,
maxNumber: 1,
showPreviewNumber: false,
api: uploadApi,
},
ifShow: ({ values }) => {
return values.type == 2
},
},
{
field: 'content',
label: '地址',
required: true,
component: 'Input',
ifShow: ({ values }) => {
return values.type == 1
},
},
]