Skip to content

Commit 408c4bb

Browse files
committed
feat: modstart update
1 parent f2fab85 commit 408c4bb

File tree

111 files changed

+932
-253
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+932
-253
lines changed

module/Vendor/Asset/entry/all.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

module/Vendor/Asset/entry/quickRunCustomFieldEdit.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

module/Vendor/Asset/entry/quickRunImageDesign.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

module/Vendor/QuickRun/ImageDesign/ImageDesignUtil.php

Lines changed: 168 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
use ModStart\Core\Provider\FontProvider;
1111
use ModStart\Core\Util\ColorUtil;
1212
use ModStart\Core\Util\FileUtil;
13+
use ModStart\Core\Util\LogUtil;
1314
use ModStart\Core\Util\QrcodeUtil;
14-
use ModStart\Core\Util\SerializeUtil;
1515

1616
class ImageDesignUtil
1717
{
@@ -66,24 +66,131 @@ public static function configSaveCheck($imageConfig)
6666
BizException::throwsIf('背景图和背景色同时为空', empty($imageConfig['backgroundImage']) && empty($imageConfig['backgroundColor']));
6767
}
6868

69-
public static function render($imageConfig, $variables = [])
69+
private static function getTextWidth($text, $fontPath, $fontSize)
7070
{
71-
BizException::throwsIfEmpty('imageConfig 为空', $imageConfig);
71+
$box = @imagettfbbox($fontSize, 0, $fontPath, $text);
72+
$width = abs($box[2] - $box[0]);
73+
return $width;
74+
}
75+
76+
private static function ttfHasChar($fontFile, $char)
77+
{
78+
$fp = fopen($fontFile, 'rb');
79+
if (!$fp) return false;
80+
fseek($fp, 4);
81+
$numTables = unpack('n', fread($fp, 2))[1];
82+
fseek($fp, 12);
83+
$cmapOffset = null;
84+
for ($i = 0; $i < $numTables; $i++) {
85+
$record = unpack('a4tag/NcheckSum/Noffset/Nlength', fread($fp, 16));
86+
if ($record['tag'] === 'cmap') {
87+
$cmapOffset = $record['offset'];
88+
break;
89+
}
90+
}
91+
92+
if ($cmapOffset === null) return false;
93+
94+
// 读取 cmap 表头
95+
fseek($fp, $cmapOffset);
96+
$cmapHeader = unpack('nversion/nnumTables', fread($fp, 4));
97+
98+
$unicodeOffset = null;
99+
for ($i = 0; $i < $cmapHeader['numTables']; $i++) {
100+
$entry = unpack('nplatformID/nencodingID/Noffset', fread($fp, 8));
101+
if ($entry['platformID'] == 3 && $entry['encodingID'] == 1) { // Windows Unicode BMP
102+
$unicodeOffset = $entry['offset'];
103+
break;
104+
}
105+
}
106+
107+
if ($unicodeOffset === null) return false;
108+
fseek($fp, $cmapOffset + $unicodeOffset);
109+
$format = unpack('nformat', fread($fp, 2))['format'];
110+
111+
if ($format != 4) {
112+
fclose($fp);
113+
return false;
114+
}
115+
116+
// 解析 format 4
117+
fseek($fp, -2, SEEK_CUR);
118+
$fmt4 = unpack('nformat/nlength/nlanguage/nsegCountX2/nsearchRange/nentrySelector/nrangeShift', fread($fp, 14));
119+
$segCount = $fmt4['segCountX2'] / 2;
120+
121+
$endCodes = unpack("n$segCount", fread($fp, $segCount * 2));
122+
fseek($fp, 2, SEEK_CUR); // skip reservedPad
123+
$startCodes = unpack("n$segCount", fread($fp, $segCount * 2));
124+
$idDeltas = unpack("n$segCount", fread($fp, $segCount * 2));
125+
126+
fclose($fp);
127+
128+
$code = mb_ord($char, 'UTF-8');
129+
for ($i = 1; $i <= $segCount; $i++) {
130+
if ($code >= $startCodes[$i] && $code <= $endCodes[$i]) {
131+
return true;
132+
}
133+
}
134+
return false;
135+
}
136+
137+
private static function getTtfFont($fontFile, $fallbackFontFile, $text)
138+
{
139+
$chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
140+
foreach ($chars as $char) {
141+
if (!self::ttfHasChar($fontFile, $char)) {
142+
return $fallbackFontFile;
143+
}
144+
}
145+
return $fontFile;
146+
}
147+
148+
private static function replaceParam($data, $variables)
149+
{
150+
if (empty($variables)) {
151+
return $data;
152+
}
153+
if (is_array($data)) {
154+
$newData = [];
155+
foreach ($data as $k => $v) {
156+
$newData[$k] = self::replaceParam($v, $variables);
157+
}
158+
return $newData;
159+
}
160+
if (is_string($data)) {
161+
foreach ($variables as $k => $v) {
162+
$data = str_replace('${' . $k . '}', $v, $data);
163+
}
164+
return $data;
165+
}
166+
return $data;
167+
}
168+
169+
public static function render($imageConfigJson, $variables = [])
170+
{
171+
BizException::throwsIfEmpty('imageConfig 为空', $imageConfigJson);
72172
$configParam = [];
73173
foreach ($variables as $k => $v) {
74174
$configParam['${' . $k . '}'] = $v;
75175
}
76-
$imageConfig = SerializeUtil::jsonEncode($imageConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
77-
$imageConfig = str_replace(array_keys($configParam), array_values($configParam), $imageConfig);
78-
$imageConfig = json_decode($imageConfig, true);
176+
//$imageConfig = SerializeUtil::jsonEncode($imageConfigJson, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
177+
//$imageConfig = str_replace(array_keys($configParam), array_values($configParam), $imageConfig);
178+
//LogUtil::info('xxxxx', [
179+
// '$imageConfigJson' => $imageConfigJson,
180+
// '$imageConfig' => $imageConfig,
181+
// '$imageConfigNew' => self::replaceParam($imageConfigJson, $variables)
182+
//]);
183+
//$imageConfig = json_decode($imageConfig, true);
184+
$imageConfig = self::replaceParam($imageConfigJson, $variables);
79185

80186
BizException::throwsIf('width empty', empty($imageConfig['width']));
81187
BizException::throwsIf('height empty', empty($imageConfig['height']));
82188
BizException::throwsIf('backgroundImage 和 backgroundColor 为空', empty($imageConfig['backgroundImage']) && empty($imageConfig['backgroundColor']));
83189
BizException::throwsIf('blocks empty', !isset($imageConfig['blocks']));
84190

191+
$systemFontPath = FontProvider::firstLocalPathOrFail();
85192
if (empty($imageConfig['font'])) {
86-
$fontPath = FontProvider::firstLocalPathOrFail();
193+
$fontPath = $systemFontPath;
87194
} else {
88195
$fontPath = FileUtil::savePathToLocalTemp($imageConfig['font'], 'ttf', true);
89196
}
@@ -101,7 +208,44 @@ public static function render($imageConfig, $variables = [])
101208
switch ($item['type']) {
102209
case 'text':
103210
$lineHeight = isset($item['data']['lineHeight']) ? $item['data']['lineHeight'] : 1.2;
104-
$lines = explode(self::LINE_BREAK, $item['data']['text']);
211+
$textFontPath = empty($item['data']['font']) ? null : $item['data']['font'];
212+
if ($textFontPath) {
213+
$textFontPath = FileUtil::savePathToLocalTemp($textFontPath, 'ttf', true);
214+
}
215+
if (empty($textFontPath)) {
216+
$textFontPath = $fontPath;
217+
}
218+
$textFontPath = self::getTtfFont($textFontPath, $systemFontPath, $item['data']['text']);
219+
$linesForBreak = explode(self::LINE_BREAK, $item['data']['text']);
220+
$lines = [];
221+
foreach ($linesForBreak as $line) {
222+
$parts = explode("\n", $line);
223+
foreach ($parts as $part) {
224+
$lines[] = $part;
225+
}
226+
}
227+
$lines = array_filter(array_map('trim', $lines));
228+
if (!empty($item['data']['width'])) {
229+
$newLines = [];
230+
foreach ($lines as $text) {
231+
$currentLine = '';
232+
$words = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
233+
foreach ($words as $char) {
234+
$testLine = $currentLine . $char;
235+
$lineWidth = self::getTextWidth($testLine, $textFontPath, $item['data']['size']);
236+
if ($lineWidth > $item['data']['width'] && $currentLine !== '') {
237+
$newLines[] = $currentLine;
238+
$currentLine = $char;
239+
} else {
240+
$currentLine = $testLine;
241+
}
242+
}
243+
if ($currentLine !== '') {
244+
$newLines[] = $currentLine;
245+
}
246+
}
247+
$lines = $newLines;
248+
}
105249
$offsets = [];
106250
if (!empty($item['data']['shadowOffset'])) {
107251
if (empty($item['data']['shadowColor'])) {
@@ -139,13 +283,22 @@ public static function render($imageConfig, $variables = [])
139283
if (empty($line)) {
140284
continue;
141285
}
142-
$image->text($line, $item['x'] + $offset['x'], $y + $offset['y'], function ($font) use ($item, $offset, $fontPath) {
143-
$font->file($fontPath);
144-
$font->size($item['data']['size']);
145-
$font->color($offset['color']);
146-
$font->align($item['data']['align']);
147-
$font->valign('top');
148-
});
286+
//LogUtil::info('xxxx', [
287+
// '$line' => $line,
288+
// '$x' => $item['x'] + $offset['x'],
289+
// '$y' => $y + $offset['y'],
290+
// 'align' => $item['data']['align'],
291+
//]);
292+
$image->text(
293+
$line, $item['x'] + $offset['x'], $y + $offset['y'],
294+
function ($font) use ($item, $offset, $textFontPath) {
295+
$font->file($textFontPath);
296+
$font->size($item['data']['size']);
297+
$font->color($offset['color']);
298+
$font->align($item['data']['align']);
299+
$font->valign('top');
300+
}
301+
);
149302
$y += $item['data']['size'] * $lineHeight;
150303
}
151304
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<template>
2+
<div class="pb-rich-view-editor" :class="{'active':editing}">
3+
<div>
4+
<div ref="editorContainer"></div>
5+
<div v-if="editing" class="tw-pt-1">
6+
<a href="javascript:;"
7+
@click="doConfirm"
8+
class="btn btn-block btn-round btn-sm btn-primary">
9+
<i class="iconfont icon-check-simple"></i> 确认
10+
</a>
11+
</div>
12+
</div>
13+
<div v-if="!editing" @click="doRichViewEdit"
14+
class="tw-overflow-auto"
15+
:style="{maxHeight:maxHeight}">
16+
<div class="ub-html" v-if="modelValue" v-html="modelValue"></div>
17+
<div class="ub-html tw-text-gray-400" v-else>
18+
<p>
19+
<i class="iconfont icon-edit"></i>
20+
点击编辑内容
21+
</p>
22+
</div>
23+
</div>
24+
<div v-if="richViewEditorShow"
25+
style="visibility:hidden;height:1px;width:1px;opacity:0;position:absolute;top:0;right:0;"
26+
id="richViewEditorPlaceholder">
27+
<script id="richViewEditor" name="richViewEditor" type="text/plain"></script>
28+
</div>
29+
</div>
30+
</template>
31+
32+
<script>
33+
import {VModelMixin} from "@ModStartAsset/svue/lib/fields-config";
34+
35+
let DefaultServer = '';
36+
if (window.__msAdminRoot) {
37+
DefaultServer = window.__msAdminRoot + 'data/ueditor';
38+
}
39+
export default {
40+
name: "RichViewEditor",
41+
mixins: [VModelMixin],
42+
props: {
43+
server: {
44+
type: String,
45+
default: DefaultServer,
46+
},
47+
maxHeight: {
48+
type: String,
49+
default: '10em',
50+
}
51+
},
52+
data() {
53+
return {
54+
richViewEditorShow: false,
55+
editing: false,
56+
}
57+
},
58+
mounted() {
59+
this.richViewEditorInit();
60+
},
61+
methods: {
62+
richViewEditorInit() {
63+
if (window.__richViewEditor) {
64+
return;
65+
}
66+
this.richViewEditorShow = true;
67+
window.__richViewEditor = true;
68+
this.$nextTick(() => {
69+
window.__richViewEditorPlaceholder = document.getElementById('richViewEditorPlaceholder');
70+
window.__richViewEditor = window.api.editor.basic('richViewEditor', {
71+
server: this.server,
72+
ready: () => {
73+
window.__richViewEditor.setContent(window.__richViewEditorValue || '');
74+
window.__richViewEditor.focus(true)
75+
}
76+
})
77+
});
78+
},
79+
doRichViewEdit() {
80+
if (this.editing) {
81+
return;
82+
}
83+
if (window.__richViewEditorEnd) {
84+
window.__richViewEditorEnd();
85+
}
86+
window.__richViewEditorEnd = () => {
87+
this.doConfirm()
88+
};
89+
this.editing = true;
90+
window.__richViewEditorValue = this.modelValue;
91+
$(this.$refs.editorContainer).html(window.__richViewEditor.container.parentNode);
92+
// this.htmlEditor.unsetFloating()
93+
window.__richViewEditor.fireEvent("beforefullscreenchange", true)
94+
window.__richViewEditor.reset()
95+
},
96+
doConfirm() {
97+
this.editing = false;
98+
this.modelValue = window.__richViewEditor.getContent();
99+
$(window.__richViewEditorPlaceholder).append(window.__richViewEditor.container.parentNode);
100+
window.__richViewEditorEnd = null;
101+
}
102+
}
103+
}
104+
</script>
105+
<style lang="less">
106+
.pb-rich-view-editor {
107+
border: 1px dashed #DDD;
108+
min-height: 1em;
109+
border-radius: 0.25rem;
110+
cursor: pointer;
111+
padding: 0.1rem;
112+
113+
&:hover, &.active {
114+
border-color: var(--color-primary);
115+
}
116+
}
117+
</style>

module/Vendor/resources/asset/src/components/all.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import FileSelector from "@ModStartAsset/svue/components/FileSelector.vue"
66
import FilesSelector from "@ModStartAsset/svue/components/FilesSelector.vue"
77
import CodeEditor from "@ModStartAsset/svue/components/CodeEditor.vue"
88
import RichEditor from "./RichEditor"
9+
import RichViewEditor from "./RichViewEditor"
910
import NameValueListEditor from "./NameValueListEditor"
1011
import SmartLink from "@ModStartAsset/svue/components/SmartLink"
1112
import SmartCaptcha from "@ModStartAsset/svue/components/SmartCaptcha"
@@ -71,6 +72,7 @@ export default (Vue) => {
7172
Vue.component("audio-selector", AudioSelector)
7273
Vue.component("code-editor", CodeEditor)
7374
Vue.component('rich-editor', RichEditor)
75+
Vue.component('rich-view-editor', RichViewEditor)
7476
Vue.component('name-value-list-editor', NameValueListEditor)
7577
Vue.component("smart-link", SmartLink)
7678
Vue.component("smart-captcha", SmartCaptcha)

0 commit comments

Comments
 (0)