TextToolComp.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. import {
  2. IconStrikethrough,
  3. IconTextBold,
  4. IconTextCenter,
  5. IconTextItalic,
  6. IconTextJustify,
  7. IconTextLeft,
  8. IconTextLetterSpacing,
  9. IconTextLineHeight,
  10. IconTextRight,
  11. IconTextSize,
  12. IconTextUnderline,
  13. } from "@/assets/icons";
  14. import { css } from "@linaria/core";
  15. import { Button, InputNumber, Tooltip, Checkbox } from "ant-design-vue";
  16. import { useEditor } from "@/modules/editor";
  17. import Select from "@queenjs-modules/queditor/components/FormUI/Items/Select";
  18. import "@simonwep/pickr/dist/themes/nano.min.css";
  19. import _ from "lodash";
  20. import { queenApi } from "queenjs";
  21. import {
  22. defineComponent,
  23. onMounted,
  24. onUnmounted,
  25. reactive,
  26. ref,
  27. toRaw,
  28. watch,
  29. } from "vue";
  30. import { any, bool, func, number, object, string } from "vue-types";
  31. import Slider from "../../formItems/Slider";
  32. import NewColorPicker from "../../formItems/NewColorPicker";
  33. interface ColumnItem {
  34. label?: string;
  35. component?: ((...args: any[]) => any) | Record<string, any>;
  36. dataIndex?: string;
  37. props?: { [name: string]: any };
  38. itemProps?: { [name: string]: any };
  39. changeExtra?: (data: any) => any;
  40. }
  41. export const TextColor = defineComponent({
  42. props: {
  43. value: string().def("#666666"),
  44. },
  45. emits: ["change"],
  46. setup(props, { emit }) {
  47. const state = reactive({
  48. color: props.value,
  49. });
  50. watch(
  51. () => props.value,
  52. () => {
  53. state.color = props.value;
  54. }
  55. );
  56. const colorChange = (value: string) => {
  57. emit("change", value);
  58. state.color = value;
  59. };
  60. return () => (
  61. <NewColorPicker
  62. value={state.color}
  63. onChange={colorChange}
  64. showGradient={false}
  65. />
  66. );
  67. },
  68. });
  69. export const AlignComp = defineComponent({
  70. props: {
  71. value: string<"left" | "right" | "center" | "justify">().def("left"),
  72. },
  73. emits: ["change"],
  74. setup(props, { emit }) {
  75. const aligns = [
  76. {
  77. label: "左对齐",
  78. key: "left",
  79. icon: <IconTextLeft />,
  80. },
  81. {
  82. label: "居中对齐",
  83. key: "center",
  84. icon: <IconTextCenter />,
  85. },
  86. {
  87. label: "右对齐",
  88. key: "right",
  89. icon: <IconTextRight />,
  90. },
  91. {
  92. label: "两端对齐",
  93. key: "justify",
  94. icon: <IconTextJustify />,
  95. },
  96. ];
  97. return () => (
  98. <div class={AlignCompWapper}>
  99. {aligns.map((e: any) => {
  100. return (
  101. <Tooltip title={e.label} placement="top">
  102. <Button
  103. class={props.value == e.key ? currStyle : null}
  104. icon={e.icon}
  105. type="text"
  106. onClick={() => {
  107. emit("change", e.key);
  108. }}
  109. ></Button>
  110. </Tooltip>
  111. );
  112. })}
  113. </div>
  114. );
  115. },
  116. });
  117. export const LetterSpacingComp = defineComponent({
  118. props: {
  119. value: any<string | number>().def(0),
  120. },
  121. emits: ["change"],
  122. setup(props, { emit }) {
  123. return () => {
  124. const value =
  125. typeof props.value === "string" ? parseInt(props.value) : props.value;
  126. return (
  127. <InputNumber
  128. prefix={<IconTextLetterSpacing class="text-22px mr-6px" />}
  129. defaultValue={0}
  130. min={0}
  131. max={100}
  132. step={1}
  133. value={value}
  134. onChange={(value: any) => {
  135. if (!value) {
  136. emit("change", "0px");
  137. return;
  138. }
  139. emit("change", value + "px");
  140. }}
  141. />
  142. );
  143. };
  144. },
  145. });
  146. export const LineHeightComp = defineComponent({
  147. props: {
  148. value: any<string | number>().def(1.5),
  149. },
  150. emits: ["change"],
  151. setup(props, { emit }) {
  152. return () => {
  153. const value =
  154. typeof props.value === "string" ? parseFloat(props.value) : props.value;
  155. return (
  156. <InputNumber
  157. prefix={<IconTextLineHeight class="text-22px mr-6px" />}
  158. defaultValue={1.5}
  159. min={0.5}
  160. max={3}
  161. step={0.5}
  162. value={value || 1.5}
  163. onChange={(value: any) => {
  164. if (!value) {
  165. emit("change", 1.5);
  166. return;
  167. }
  168. emit("change", value);
  169. }}
  170. />
  171. );
  172. };
  173. },
  174. });
  175. export const FontStyleWapper = defineComponent({
  176. emits: ["change"],
  177. setup(props, { emit }) {
  178. const fontStyleColumns = [
  179. {
  180. label: "加粗",
  181. dataIndex: "bold",
  182. icon: <IconTextBold />,
  183. },
  184. {
  185. label: "斜体",
  186. dataIndex: "italic",
  187. icon: <IconTextItalic />,
  188. },
  189. {
  190. label: "下划线",
  191. dataIndex: "underline",
  192. icon: <IconTextUnderline />,
  193. },
  194. {
  195. label: "删除线",
  196. dataIndex: "strikethrough",
  197. icon: <IconStrikethrough />,
  198. },
  199. ];
  200. const changeVal = (e: any) => {
  201. emit("change", e);
  202. };
  203. return () => (
  204. <div class={[FontStyleCompWapper]}>
  205. {fontStyleColumns.map((e: any) => {
  206. return (
  207. <TextToolItem
  208. key={e.dataIndex}
  209. class={"!mr-0"}
  210. column={{
  211. label: e.label,
  212. dataIndex: e.dataIndex,
  213. component: (props) => {
  214. return <FontStyleComp icon={e.icon} {...props} />;
  215. },
  216. }}
  217. onChange={changeVal}
  218. />
  219. );
  220. })}
  221. </div>
  222. );
  223. },
  224. });
  225. export const FontStyleComp = defineComponent({
  226. props: {
  227. icon: any(),
  228. value: bool().def(false),
  229. },
  230. emits: ["change"],
  231. setup(props, { emit }) {
  232. return () => {
  233. return (
  234. <Button
  235. type="text"
  236. class={props.value ? currStyle : null}
  237. icon={props.icon}
  238. onClick={() => {
  239. emit("change", !props.value);
  240. }}
  241. ></Button>
  242. );
  243. };
  244. },
  245. });
  246. export const FontFamily = defineComponent({
  247. props: {
  248. value: string().def(""),
  249. },
  250. emits: ["change"],
  251. setup(props, { emit }) {
  252. const options = [
  253. { label: "默认字体", value: "" },
  254. { label: "宋体", value: "宋体,Songti,STSong,serif" },
  255. { label: "黑体", value: "黑体,Heiti,STHeiti,sans-serif" },
  256. { label: "仿宋", value: "仿宋,FangSong,STFangsong,serif" },
  257. { label: "楷体", value: "楷体,KaiTi,STKaiti,sans-serif" },
  258. ];
  259. return () => {
  260. return (
  261. <Select
  262. options={options}
  263. value={props.value || ""}
  264. onChange={(v) => {
  265. emit("change", v);
  266. }}
  267. ></Select>
  268. );
  269. };
  270. },
  271. });
  272. export const FontSize = defineComponent({
  273. props: {
  274. value: any().def("12px"),
  275. },
  276. emits: ["change"],
  277. setup(props, { emit }) {
  278. return () => {
  279. return (
  280. <InputNumber
  281. prefix={<IconTextSize class="text-22px mr-6px" />}
  282. defaultValue={12}
  283. min={12}
  284. max={60}
  285. value={parseInt(props.value) || 12}
  286. onChange={(value: any) => {
  287. if (!value) {
  288. emit("change", "12px");
  289. return;
  290. }
  291. emit("change", value + "px");
  292. }}
  293. />
  294. );
  295. };
  296. },
  297. });
  298. export const TextStroke = defineComponent({
  299. props: {
  300. value: any().def(undefined),
  301. },
  302. emits: ["change"],
  303. setup(props, { emit }) {
  304. const state = reactive({
  305. visible: props.value ? true : false,
  306. width: 1,
  307. color: "#666666",
  308. });
  309. watch(
  310. () => props.value,
  311. () => {
  312. formatVal(props.value);
  313. }
  314. );
  315. const formatVal = (value: any) => {
  316. if (!value) {
  317. state.visible = false;
  318. state.color = "#666666";
  319. state.width = 1;
  320. return;
  321. }
  322. state.visible = true;
  323. const colorReg = /#[a-zA-Z0-9]{6}/g;
  324. const color = value.match(colorReg)[0];
  325. state.color = color;
  326. const widthReg = /\d+px/g;
  327. const width = value.match(widthReg)[0];
  328. state.width = parseInt(width);
  329. };
  330. const colorChange = (v: string) => {
  331. state.color = v;
  332. buildValueSub();
  333. };
  334. const visibleChange = (e: any) => {
  335. const checked = e.target.checked;
  336. state.visible = checked;
  337. buildValueSub();
  338. };
  339. const widthChange = (e: any) => {
  340. state.width = e;
  341. buildValueSub();
  342. };
  343. const buildValueSub = () => {
  344. if (!state.visible) {
  345. return;
  346. }
  347. const value = `${state.width}px ${state.color}`;
  348. emit("change", value);
  349. };
  350. return () => {
  351. return (
  352. <div class={"flex-1"}>
  353. <div class={"flex justify-between items-center w-full"}>
  354. <div class={"flex-1 flex items-center"}>
  355. <Checkbox checked={state.visible} onChange={visibleChange} />
  356. {state.visible && (
  357. <div class={"flex-1 px-20px"}>
  358. <Tooltip title={"描边宽度"} placement="top">
  359. <InputNumber
  360. min={1}
  361. max={5}
  362. step={1}
  363. value={state.width}
  364. onChange={widthChange}
  365. />
  366. </Tooltip>
  367. </div>
  368. )}
  369. </div>
  370. <Tooltip title={"描边颜色"} placement="top">
  371. <TextColor value={state.color} onChange={colorChange} />
  372. </Tooltip>
  373. </div>
  374. </div>
  375. );
  376. };
  377. },
  378. });
  379. export const LinkButton = defineComponent({
  380. props: {
  381. icon: any(),
  382. value: any(),
  383. },
  384. emits: ["change"],
  385. setup(props, { emit }) {
  386. const showLinkInput = async () => {
  387. const res = await queenApi.showInput({
  388. title: "请输入链接地址",
  389. defaultValue: "http://",
  390. });
  391. emit("change", res);
  392. };
  393. return () => (
  394. <Button type="text" icon={props.icon} onClick={showLinkInput}></Button>
  395. );
  396. },
  397. });
  398. export const TextToolItem = defineComponent({
  399. props: {
  400. column: object<ColumnItem>(),
  401. index: number(),
  402. onChange: func(),
  403. },
  404. setup(props) {
  405. const state = reactive({
  406. value: undefined,
  407. });
  408. const { controls } = useEditor();
  409. let editor: any = null;
  410. watch(
  411. () => controls.textEditorCtrl.state.currEditor,
  412. () => {
  413. editor = toRaw(controls.textEditorCtrl.state.currEditor);
  414. initCommands();
  415. }
  416. );
  417. function handleValueChange() {
  418. const { column } = props;
  419. if (!editor) {
  420. return;
  421. }
  422. const command = editor.commands.get(column?.dataIndex);
  423. if (command) {
  424. state.value = command.value;
  425. }
  426. }
  427. const initCommands = () => {
  428. const { column } = props;
  429. if (!editor) {
  430. return;
  431. }
  432. const command = editor.commands.get(column?.dataIndex);
  433. if (command) {
  434. console.log("init", column?.dataIndex, command.value);
  435. state.value = command.value;
  436. command.on("change:value", handleValueChange);
  437. }
  438. };
  439. onMounted(() => {
  440. initCommands();
  441. });
  442. onUnmounted(() => {
  443. const { column } = props;
  444. if (!editor) {
  445. return;
  446. }
  447. const command = editor.commands.get(column?.dataIndex);
  448. if (command) {
  449. command.off("change:value", handleValueChange);
  450. }
  451. });
  452. const changeVal = (value: any, ...args: any[]) => {
  453. const { column } = props;
  454. let params = {
  455. dataIndex: column?.dataIndex,
  456. value: { value },
  457. ...args,
  458. };
  459. if (column?.changeExtra) params = column.changeExtra?.(params);
  460. props.onChange?.(params);
  461. return params;
  462. };
  463. const component = props.column?.component || null;
  464. return () => {
  465. const { column, index } = props;
  466. return (
  467. <div
  468. key={column?.dataIndex || "" + index}
  469. class={formItemStyles}
  470. {...column?.itemProps}
  471. onClick={(e) => e.stopPropagation()}
  472. >
  473. {column?.label ? (
  474. <Tooltip title={column.label} placement="top">
  475. <component
  476. value={state.value}
  477. {...column.props}
  478. onChange={changeVal}
  479. />
  480. </Tooltip>
  481. ) : (
  482. <component
  483. value={state.value}
  484. {...column?.props}
  485. onChange={changeVal}
  486. />
  487. )}
  488. </div>
  489. );
  490. };
  491. },
  492. });
  493. const currStyle = css`
  494. color: @inf-primary-color;
  495. &:hover,
  496. &:focus {
  497. color: @inf-primary-color;
  498. }
  499. `;
  500. const ColorPicker = css`
  501. position: relative;
  502. width: 32px;
  503. height: 32px;
  504. border-radius: 2px;
  505. cursor: pointer;
  506. .color_picker {
  507. width: 100%;
  508. height: 100%;
  509. border-radius: 2px;
  510. border: 1px solid transparent;
  511. &:focus,
  512. &:hover {
  513. border-color: @inf-primary-color;
  514. box-shadow: 0 0 0 2px rgba(232, 139, 0, 0.2);
  515. }
  516. }
  517. .color_input {
  518. position: absolute;
  519. left: 0;
  520. bottom: 0;
  521. width: 100%;
  522. height: 0;
  523. padding: 0;
  524. border: none;
  525. visibility: hidden;
  526. }
  527. `;
  528. const AlignCompWapper = css`
  529. display: flex;
  530. background-color: #303030;
  531. .ant-btn {
  532. flex: 1;
  533. width: 100%;
  534. line-height: 1;
  535. .inficon {
  536. font-size: 22px;
  537. }
  538. }
  539. `;
  540. const FontStyleCompWapper = css`
  541. flex: 1;
  542. display: flex;
  543. align-items: center;
  544. margin-right: 12px;
  545. border-radius: 2px;
  546. background-color: #303030;
  547. & > div {
  548. flex: 1;
  549. border-radius: 0;
  550. .ant-btn {
  551. width: 100%;
  552. line-height: 1;
  553. .inficon {
  554. font-size: 22px;
  555. }
  556. }
  557. }
  558. `;
  559. const formItemStyles = css`
  560. height: 100%;
  561. flex: 1;
  562. margin-right: 12px;
  563. .ant-input-number-affix-wrapper,
  564. .ant-select {
  565. background-color: #303030;
  566. }
  567. border-radius: 2px;
  568. &:last-child {
  569. margin-right: 0;
  570. }
  571. &.disabled {
  572. cursor: not-allowed;
  573. }
  574. `;