|
@@ -0,0 +1,513 @@
|
|
|
+import {
|
|
|
+ IconStrikethrough,
|
|
|
+ IconTextBold,
|
|
|
+ IconTextCenter,
|
|
|
+ IconTextItalic,
|
|
|
+ IconTextJustify,
|
|
|
+ IconTextLeft,
|
|
|
+ IconTextLetterSpacing,
|
|
|
+ IconTextLineHeight,
|
|
|
+ IconTextRight,
|
|
|
+ IconTextSize,
|
|
|
+ IconTextUnderline,
|
|
|
+} from "@/assets/icons";
|
|
|
+import { css } from "@linaria/core";
|
|
|
+import { Button, InputNumber, Tooltip } from "ant-design-vue";
|
|
|
+
|
|
|
+import { useEditor } from "@/modules/editor";
|
|
|
+import Select from "@queenjs-modules/queditor/components/FormUI/Items/Select";
|
|
|
+import "@simonwep/pickr/dist/themes/nano.min.css";
|
|
|
+import _ from "lodash";
|
|
|
+import { queenApi } from "queenjs";
|
|
|
+import {
|
|
|
+ defineComponent,
|
|
|
+ onMounted,
|
|
|
+ onUnmounted,
|
|
|
+ reactive,
|
|
|
+ ref,
|
|
|
+ toRaw,
|
|
|
+ watch,
|
|
|
+} from "vue";
|
|
|
+import { any, bool, func, number, object, string } from "vue-types";
|
|
|
+interface ColumnItem {
|
|
|
+ label?: string;
|
|
|
+ component?: ((...args: any[]) => any) | Record<string, any>;
|
|
|
+ dataIndex?: string;
|
|
|
+ props?: { [name: string]: any };
|
|
|
+ itemProps?: { [name: string]: any };
|
|
|
+ changeExtra?: (data: any) => any;
|
|
|
+}
|
|
|
+
|
|
|
+export const TextColor = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: string().def("#666666"),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ let pickerRef = ref();
|
|
|
+
|
|
|
+ const state = reactive({
|
|
|
+ color: props.value,
|
|
|
+ });
|
|
|
+ watch(
|
|
|
+ () => props.value,
|
|
|
+ () => {
|
|
|
+ state.color = props.value;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ const colorChange = (e: any) => {
|
|
|
+ const hexa = e.target.value;
|
|
|
+ emit("change", hexa);
|
|
|
+ state.color = hexa;
|
|
|
+ };
|
|
|
+
|
|
|
+ return () => (
|
|
|
+ <div
|
|
|
+ class={ColorPicker}
|
|
|
+ onClick={() => {
|
|
|
+ pickerRef?.value.click();
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <div class="color_picker" style={{ backgroundColor: state.color }}>
|
|
|
+ <input
|
|
|
+ type="color"
|
|
|
+ class="color_input"
|
|
|
+ ref={pickerRef}
|
|
|
+ value={state.color}
|
|
|
+ onInput={_.debounce(colorChange, 300)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+});
|
|
|
+export const AlignComp = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: string<"left" | "right" | "center" | "justify">().def("left"),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const aligns = [
|
|
|
+ {
|
|
|
+ label: "左对齐",
|
|
|
+ key: "left",
|
|
|
+ icon: <IconTextLeft />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "居中对齐",
|
|
|
+ key: "center",
|
|
|
+ icon: <IconTextCenter />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "右对齐",
|
|
|
+ key: "right",
|
|
|
+ icon: <IconTextRight />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "两端对齐",
|
|
|
+ key: "justify",
|
|
|
+ icon: <IconTextJustify />,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ return () => (
|
|
|
+ <div class={AlignCompWapper}>
|
|
|
+ {aligns.map((e: any) => {
|
|
|
+ return (
|
|
|
+ <Tooltip title={e.label} placement="top">
|
|
|
+ <Button
|
|
|
+ class={props.value == e.key ? currStyle : null}
|
|
|
+ icon={e.icon}
|
|
|
+ type="text"
|
|
|
+ onClick={() => {
|
|
|
+ emit("change", e.key);
|
|
|
+ }}
|
|
|
+ ></Button>
|
|
|
+ </Tooltip>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+});
|
|
|
+export const LetterSpacingComp = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: any<string | number>().def(0),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ return () => {
|
|
|
+ const value =
|
|
|
+ typeof props.value === "string" ? parseInt(props.value) : props.value;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <InputNumber
|
|
|
+ prefix={<IconTextLetterSpacing class="text-22px mr-6px" />}
|
|
|
+ defaultValue={0}
|
|
|
+ min={0}
|
|
|
+ max={100}
|
|
|
+ step={1}
|
|
|
+ value={value}
|
|
|
+ onChange={(value: any) => {
|
|
|
+ if (!value) {
|
|
|
+ emit("change", "0px");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ emit("change", value + "px");
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const LineHeightComp = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: any<string | number>().def(1.5),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ return () => {
|
|
|
+ const value =
|
|
|
+ typeof props.value === "string" ? parseFloat(props.value) : props.value;
|
|
|
+ return (
|
|
|
+ <InputNumber
|
|
|
+ prefix={<IconTextLineHeight class="text-22px mr-6px" />}
|
|
|
+ defaultValue={1.5}
|
|
|
+ min={0.5}
|
|
|
+ max={3}
|
|
|
+ step={0.5}
|
|
|
+ value={value || 1.5}
|
|
|
+ onChange={(value: any) => {
|
|
|
+ if (!value) {
|
|
|
+ emit("change", 1.5);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ emit("change", value);
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+export const FontStyleWapper = defineComponent({
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const fontStyleColumns = [
|
|
|
+ {
|
|
|
+ label: "加粗",
|
|
|
+ dataIndex: "bold",
|
|
|
+ icon: <IconTextBold />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "斜体",
|
|
|
+ dataIndex: "italic",
|
|
|
+ icon: <IconTextItalic />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "下划线",
|
|
|
+ dataIndex: "underline",
|
|
|
+ icon: <IconTextUnderline />,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "删除线",
|
|
|
+ dataIndex: "strikethrough",
|
|
|
+ icon: <IconStrikethrough />,
|
|
|
+ },
|
|
|
+ ];
|
|
|
+ const changeVal = (e: any) => {
|
|
|
+ emit("change", e);
|
|
|
+ };
|
|
|
+ return () => (
|
|
|
+ <div class={[FontStyleCompWapper]}>
|
|
|
+ {fontStyleColumns.map((e: any) => {
|
|
|
+ return (
|
|
|
+ <TextToolItem
|
|
|
+ key={e.dataIndex}
|
|
|
+ class={"!mr-0"}
|
|
|
+ column={{
|
|
|
+ label: e.label,
|
|
|
+ dataIndex: e.dataIndex,
|
|
|
+ component: (props) => {
|
|
|
+ return <FontStyleComp icon={e.icon} {...props} />;
|
|
|
+ },
|
|
|
+ }}
|
|
|
+ onChange={changeVal}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+});
|
|
|
+export const FontStyleComp = defineComponent({
|
|
|
+ props: {
|
|
|
+ icon: any(),
|
|
|
+ value: bool().def(false),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ return () => {
|
|
|
+ return (
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ class={props.value ? currStyle : null}
|
|
|
+ icon={props.icon}
|
|
|
+ onClick={() => {
|
|
|
+ emit("change", !props.value);
|
|
|
+ }}
|
|
|
+ ></Button>
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const FontFamily = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: string().def(""),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const options = [
|
|
|
+ { label: "默认字体", value: "" },
|
|
|
+ { label: "宋体", value: "宋体,Songti,STSong,serif" },
|
|
|
+ { label: "黑体", value: "黑体,Heiti,STHeiti,sans-serif" },
|
|
|
+ { label: "仿宋", value: "仿宋,FangSong,STFangsong,serif" },
|
|
|
+ { label: "楷体", value: "楷体,KaiTi,STKaiti,sans-serif" },
|
|
|
+ ];
|
|
|
+ return () => {
|
|
|
+ return (
|
|
|
+ <Select
|
|
|
+ options={options}
|
|
|
+ value={props.value || ""}
|
|
|
+ onChange={(v) => {
|
|
|
+ emit("change", v);
|
|
|
+ }}
|
|
|
+ ></Select>
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+export const FontSize = defineComponent({
|
|
|
+ props: {
|
|
|
+ value: any().def("12px"),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ return () => {
|
|
|
+ return (
|
|
|
+ <InputNumber
|
|
|
+ prefix={<IconTextSize class="text-22px mr-6px" />}
|
|
|
+ defaultValue={12}
|
|
|
+ min={12}
|
|
|
+ max={60}
|
|
|
+ value={parseInt(props.value) || 12}
|
|
|
+ onChange={(value: any) => {
|
|
|
+ if (!value) {
|
|
|
+ emit("change", "12px");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ emit("change", value + "px");
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+export const LinkButton = defineComponent({
|
|
|
+ props: {
|
|
|
+ icon: any(),
|
|
|
+ value: any(),
|
|
|
+ },
|
|
|
+ emits: ["change"],
|
|
|
+ setup(props, { emit }) {
|
|
|
+ const showLinkInput = async () => {
|
|
|
+ const res = await queenApi.showInput({
|
|
|
+ title: "请输入链接地址",
|
|
|
+ defaultValue: "http://",
|
|
|
+ });
|
|
|
+ emit("change", res);
|
|
|
+ };
|
|
|
+ return () => (
|
|
|
+ <Button type="text" icon={props.icon} onClick={showLinkInput}></Button>
|
|
|
+ );
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+export const TextToolItem = defineComponent({
|
|
|
+ props: {
|
|
|
+ column: object<ColumnItem>(),
|
|
|
+ index: number(),
|
|
|
+ onChange: func(),
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ const state = reactive({
|
|
|
+ value: undefined,
|
|
|
+ });
|
|
|
+
|
|
|
+ const { controls } = useEditor();
|
|
|
+ let editor: any = null;
|
|
|
+ watch(
|
|
|
+ () => controls.textEditorCtrl.state.currEditor,
|
|
|
+ () => {
|
|
|
+ editor = toRaw(controls.textEditorCtrl.state.currEditor);
|
|
|
+ initCommands();
|
|
|
+ }
|
|
|
+ );
|
|
|
+ function handleValueChange() {
|
|
|
+ const { column } = props;
|
|
|
+ if (!editor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const command = editor.commands.get(column?.dataIndex);
|
|
|
+ if (command) {
|
|
|
+ state.value = command.value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const initCommands = () => {
|
|
|
+ const { column } = props;
|
|
|
+ if (!editor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const command = editor.commands.get(column?.dataIndex);
|
|
|
+ if (command) {
|
|
|
+ console.log("init", column?.dataIndex, command.value);
|
|
|
+ state.value = command.value;
|
|
|
+ command.on("change:value", handleValueChange);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ onMounted(() => {
|
|
|
+ initCommands();
|
|
|
+ });
|
|
|
+ onUnmounted(() => {
|
|
|
+ const { column } = props;
|
|
|
+ if (!editor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const command = editor.commands.get(column?.dataIndex);
|
|
|
+ if (command) {
|
|
|
+ command.off("change:value", handleValueChange);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const changeVal = (value: any, ...args: any[]) => {
|
|
|
+ const { column } = props;
|
|
|
+ let params = {
|
|
|
+ dataIndex: column?.dataIndex,
|
|
|
+ value: { value },
|
|
|
+ ...args,
|
|
|
+ };
|
|
|
+ if (column?.changeExtra) params = column.changeExtra?.(params);
|
|
|
+ props.onChange?.(params);
|
|
|
+ return params;
|
|
|
+ };
|
|
|
+
|
|
|
+ const component = props.column?.component || null;
|
|
|
+ return () => {
|
|
|
+ const { column, index } = props;
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ key={column?.dataIndex || "" + index}
|
|
|
+ class={formItemStyles}
|
|
|
+ {...column?.itemProps}
|
|
|
+ onClick={(e) => e.stopPropagation()}
|
|
|
+ >
|
|
|
+ {column?.label ? (
|
|
|
+ <Tooltip title={column.label} placement="top">
|
|
|
+ <component
|
|
|
+ value={state.value}
|
|
|
+ {...column.props}
|
|
|
+ onChange={changeVal}
|
|
|
+ />
|
|
|
+ </Tooltip>
|
|
|
+ ) : (
|
|
|
+ <component
|
|
|
+ value={state.value}
|
|
|
+ {...column?.props}
|
|
|
+ onChange={changeVal}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ };
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const currStyle = css`
|
|
|
+ color: @inf-primary-color;
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ color: @inf-primary-color;
|
|
|
+ }
|
|
|
+`;
|
|
|
+const ColorPicker = css`
|
|
|
+ position: relative;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border-radius: 2px;
|
|
|
+ cursor: pointer;
|
|
|
+ .color_picker {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ border-radius: 2px;
|
|
|
+ border: 1px solid transparent;
|
|
|
+ &:focus,
|
|
|
+ &:hover {
|
|
|
+ border-color: @inf-primary-color;
|
|
|
+ box-shadow: 0 0 0 2px rgba(232, 139, 0, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .color_input {
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ bottom: 0;
|
|
|
+ width: 100%;
|
|
|
+ height: 0;
|
|
|
+ padding: 0;
|
|
|
+ border: none;
|
|
|
+ visibility: hidden;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const AlignCompWapper = css`
|
|
|
+ display: flex;
|
|
|
+ .ant-btn {
|
|
|
+ flex: 1;
|
|
|
+ width: 100%;
|
|
|
+ line-height: 1;
|
|
|
+ .inficon {
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+const FontStyleCompWapper = css`
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-right: 12px;
|
|
|
+ border-radius: 2px;
|
|
|
+ & > div {
|
|
|
+ flex: 1;
|
|
|
+ border-radius: 0;
|
|
|
+ .ant-btn {
|
|
|
+ width: 100%;
|
|
|
+ line-height: 1;
|
|
|
+ .inficon {
|
|
|
+ font-size: 22px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const formItemStyles = css`
|
|
|
+ height: 100%;
|
|
|
+ flex: 1;
|
|
|
+ margin-right: 12px;
|
|
|
+ background-color: #303030;
|
|
|
+ border-radius: 2px;
|
|
|
+ &:last-child {
|
|
|
+ margin-right: 0;
|
|
|
+ }
|
|
|
+ &.disabled {
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+`;
|