@ruixinkeji/prism-ui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +141 -0
  2. package/components/PrismAIAssist/PrismAIAssist.vue +98 -0
  3. package/components/PrismAddressInput/PrismAddressInput.vue +597 -0
  4. package/components/PrismCityCascadeSelect/PrismCityCascadeSelect.vue +793 -0
  5. package/components/PrismCityPicker/PrismCityPicker.vue +1008 -0
  6. package/components/PrismCitySelect/PrismCitySelect.vue +435 -0
  7. package/components/PrismCode/PrismCode.vue +749 -0
  8. package/components/PrismCodeInput/PrismCodeInput.vue +156 -0
  9. package/components/PrismDateTimePicker/PrismDateTimePicker.vue +953 -0
  10. package/components/PrismDropdown/PrismDropdown.vue +77 -0
  11. package/components/PrismGroupSticky/PrismGroupSticky.vue +352 -0
  12. package/components/PrismIdCardInput/PrismIdCardInput.vue +253 -0
  13. package/components/PrismImagePicker/PrismImagePicker.vue +457 -0
  14. package/components/PrismIndexBar/PrismIndexBar.vue +243 -0
  15. package/components/PrismLicensePlateInput/PrismLicensePlateInput.vue +1100 -0
  16. package/components/PrismMusicPlayer/PrismMusicPlayer.vue +530 -0
  17. package/components/PrismNavBar/PrismNavBar.vue +199 -0
  18. package/components/PrismSecureInput/PrismSecureInput.vue +360 -0
  19. package/components/PrismSticky/PrismSticky.vue +173 -0
  20. package/components/PrismSwiper/PrismSwiper.vue +339 -0
  21. package/components/PrismSwitch/PrismSwitch.vue +202 -0
  22. package/components/PrismTabBar/PrismTabBar.vue +147 -0
  23. package/components/PrismTabs/PrismTabs.vue +49 -0
  24. package/components/PrismVoiceInput/PrismVoiceInput.vue +529 -0
  25. package/index.d.ts +24 -0
  26. package/index.esm.js +25 -0
  27. package/index.js +25 -0
  28. package/package.json +89 -0
  29. package/styles/base.scss +227 -0
  30. package/styles/button.scss +120 -0
  31. package/styles/card.scss +306 -0
  32. package/styles/colors.scss +877 -0
  33. package/styles/data.scss +1229 -0
  34. package/styles/effects.scss +407 -0
  35. package/styles/feedback.scss +698 -0
  36. package/styles/form.scss +1574 -0
  37. package/styles/index.scss +46 -0
  38. package/styles/list.scss +184 -0
  39. package/styles/navigation.scss +554 -0
  40. package/styles/overlay.scss +182 -0
  41. package/styles/utilities.scss +134 -0
  42. package/styles/variables.scss +138 -0
  43. package/theme/blue.scss +36 -0
  44. package/theme/cyan.scss +32 -0
  45. package/theme/green.scss +32 -0
  46. package/theme/orange.scss +32 -0
  47. package/theme/purple.scss +32 -0
  48. package/theme/red.scss +32 -0
@@ -0,0 +1,749 @@
1
+ <template>
2
+ <view class="prism-code" :class="[actualTheme, { 'show-line-numbers': showLineNumbers, 'collapsed': !isExpanded }]">
3
+ <!-- 头部 -->
4
+ <view class="code-header" v-if="language || copyable || expandable">
5
+ <view class="header-left">
6
+ <view class="code-expand" @click="toggleExpand" v-if="expandable">
7
+ <text class="fa" :class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-right'"></text>
8
+ </view>
9
+ <text class="code-lang" v-if="language">{{ language }}</text>
10
+ </view>
11
+ <view class="header-right">
12
+ <!-- 复制按钮 -->
13
+ <view class="code-copy" @click="handleCopy" v-if="copyable">
14
+ <text class="fa" :class="copied ? 'fa-check' : 'fa-copy'"></text>
15
+ <text class="copy-text">{{ copied ? '已复制' : '复制' }}</text>
16
+ </view>
17
+ </view>
18
+ </view>
19
+
20
+ <!-- 代码内容 -->
21
+ <view class="code-body" v-show="isExpanded" @dblclick="handleCopy">
22
+ <view class="code-lines">
23
+ <template v-for="(line, index) in displayLines" :key="index">
24
+ <view class="code-line" v-if="line.visible">
25
+ <view class="fold-btn" v-if="foldable && line.foldable" @click="toggleFold(line.foldStart)">
26
+ <text class="fa" :class="line.folded ? 'fa-plus' : 'fa-minus'"></text>
27
+ </view>
28
+ <view class="fold-placeholder" v-else-if="foldable"></view>
29
+ <text class="line-number" v-if="showLineNumbers">{{ line.lineNum }}</text>
30
+ <text class="line-content" v-if="!highlight">{{ line.content }}{{ line.folded ? ' ...' : '' }}</text>
31
+ <rich-text class="line-content hl-content" v-else :nodes="line.htmlContent"></rich-text>
32
+ </view>
33
+ </template>
34
+ </view>
35
+ </view>
36
+ </view>
37
+ </template>
38
+
39
+ <script setup>
40
+ /**
41
+ * PrismCode 代码展示组件
42
+ *
43
+ * 功能特性:
44
+ * - 支持多种编程语言语法高亮
45
+ * - 支持深色/浅色主题自动切换
46
+ * - 支持代码复制、展开/收起、括号折叠
47
+ * - 支持显示行号
48
+ *
49
+ * 使用示例:
50
+ * <PrismCode code="const a = 1;" language="JavaScript" :copyable="true" />
51
+ */
52
+ import { ref, computed } from 'vue';
53
+ import { useAppStore } from '@/store/app';
54
+
55
+ const props = defineProps({
56
+ // 要展示的代码内容
57
+ code: { type: String, default: '' },
58
+
59
+ // 编程语言,用于语法高亮和头部显示
60
+ // 支持: js/ts/python/java/go/rust/cpp/sql/html/css/json/shell/vue
61
+ language: { type: String, default: '' },
62
+
63
+ // 主题模式: 'auto' | 'light' | 'dark'
64
+ // auto: 跟随系统深色模式自动切换
65
+ theme: { type: String, default: 'auto' },
66
+
67
+ // 是否显示行号
68
+ showLineNumbers: { type: Boolean, default: false },
69
+
70
+ // 是否显示复制按钮(双击代码区域也可复制)
71
+ copyable: { type: Boolean, default: false },
72
+
73
+ // 是否可展开/收起整个代码块
74
+ expandable: { type: Boolean, default: false },
75
+
76
+ // 默认是否展开(仅 expandable 为 true 时有效)
77
+ defaultExpanded: { type: Boolean, default: true },
78
+
79
+ // 是否启用括号折叠功能(折叠 {}、[]、() 内的代码)
80
+ foldable: { type: Boolean, default: false },
81
+
82
+ // 是否启用语法高亮
83
+ highlight: { type: Boolean, default: true }
84
+ });
85
+
86
+ const appStore = useAppStore();
87
+ const copied = ref(false);
88
+ const isExpanded = ref(props.defaultExpanded);
89
+ const foldedRanges = ref(new Set());
90
+
91
+ // 计算实际主题
92
+ const actualTheme = computed(() => {
93
+ if (props.theme === 'auto') {
94
+ return appStore.isDarkMode ? 'dark' : 'light';
95
+ }
96
+ return props.theme;
97
+ });
98
+
99
+ const codeLines = computed(() => {
100
+ return props.code ? props.code.split('\n') : [''];
101
+ });
102
+
103
+ // 语言关键字配置
104
+ const langKeywords = {
105
+ js: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'class', 'extends', 'import', 'export', 'from', 'default', 'new', 'this', 'super', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'yield', 'static', 'get', 'set'],
106
+ ts: ['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'class', 'extends', 'implements', 'import', 'export', 'from', 'default', 'new', 'this', 'super', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'typeof', 'instanceof', 'in', 'of', 'delete', 'void', 'yield', 'static', 'get', 'set', 'interface', 'type', 'enum', 'namespace', 'module', 'declare', 'abstract', 'readonly', 'private', 'protected', 'public', 'as', 'is', 'keyof', 'infer'],
107
+ python: ['def', 'class', 'return', 'if', 'elif', 'else', 'for', 'while', 'break', 'continue', 'pass', 'import', 'from', 'as', 'try', 'except', 'finally', 'raise', 'with', 'lambda', 'yield', 'global', 'nonlocal', 'assert', 'del', 'in', 'not', 'and', 'or', 'is', 'async', 'await'],
108
+ java: ['public', 'private', 'protected', 'static', 'final', 'abstract', 'class', 'interface', 'extends', 'implements', 'new', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'throws', 'import', 'package', 'this', 'super', 'void', 'instanceof', 'synchronized', 'volatile', 'transient', 'native', 'enum'],
109
+ go: ['func', 'return', 'if', 'else', 'for', 'range', 'switch', 'case', 'break', 'continue', 'default', 'package', 'import', 'type', 'struct', 'interface', 'map', 'chan', 'go', 'defer', 'select', 'var', 'const', 'make', 'new', 'nil', 'len', 'cap', 'append', 'copy', 'delete', 'panic', 'recover'],
110
+ rust: ['fn', 'let', 'mut', 'const', 'static', 'return', 'if', 'else', 'match', 'for', 'while', 'loop', 'break', 'continue', 'struct', 'enum', 'impl', 'trait', 'type', 'where', 'use', 'mod', 'pub', 'crate', 'self', 'super', 'as', 'in', 'ref', 'move', 'async', 'await', 'dyn', 'unsafe', 'extern'],
111
+ cpp: ['int', 'float', 'double', 'char', 'bool', 'void', 'long', 'short', 'unsigned', 'signed', 'const', 'static', 'extern', 'auto', 'register', 'volatile', 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'default', 'class', 'struct', 'union', 'enum', 'public', 'private', 'protected', 'virtual', 'override', 'final', 'new', 'delete', 'this', 'template', 'typename', 'namespace', 'using', 'try', 'catch', 'throw', 'include', 'define', 'ifdef', 'ifndef', 'endif'],
112
+ sql: ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', 'IS', 'NULL', 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'OFFSET', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'GROUP', 'HAVING', 'INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'DROP', 'ALTER', 'INDEX', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE', 'DEFAULT', 'AS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'UNION', 'ALL', 'EXISTS'],
113
+ html: ['html', 'head', 'body', 'div', 'span', 'p', 'a', 'img', 'ul', 'ol', 'li', 'table', 'tr', 'td', 'th', 'form', 'input', 'button', 'select', 'option', 'textarea', 'label', 'script', 'style', 'link', 'meta', 'title', 'header', 'footer', 'nav', 'main', 'section', 'article', 'aside', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'br', 'hr', 'strong', 'em', 'code', 'pre', 'blockquote', 'iframe', 'video', 'audio', 'canvas', 'svg'],
114
+ css: ['color', 'background', 'background-color', 'font-size', 'font-weight', 'font-family', 'margin', 'padding', 'border', 'width', 'height', 'display', 'position', 'top', 'right', 'bottom', 'left', 'flex', 'grid', 'align-items', 'justify-content', 'text-align', 'line-height', 'overflow', 'z-index', 'opacity', 'transform', 'transition', 'animation', 'box-shadow', 'border-radius', 'cursor', 'visibility', 'content', 'important'],
115
+ json: [],
116
+ shell: ['echo', 'cd', 'ls', 'mkdir', 'rm', 'cp', 'mv', 'cat', 'grep', 'find', 'chmod', 'chown', 'sudo', 'apt', 'yum', 'npm', 'yarn', 'git', 'docker', 'curl', 'wget', 'ssh', 'scp', 'tar', 'zip', 'unzip', 'export', 'source', 'alias', 'if', 'then', 'else', 'fi', 'for', 'do', 'done', 'while', 'case', 'esac', 'function', 'return', 'exit'],
117
+ vue: ['template', 'script', 'style', 'setup', 'ref', 'reactive', 'computed', 'watch', 'watchEffect', 'onMounted', 'onUnmounted', 'defineProps', 'defineEmits', 'defineExpose', 'v-if', 'v-else', 'v-for', 'v-model', 'v-bind', 'v-on', 'v-show', 'v-slot']
118
+ };
119
+
120
+ // 类型关键字
121
+ const typeKeywords = {
122
+ ts: ['string', 'number', 'boolean', 'object', 'any', 'void', 'never', 'unknown', 'Array', 'Promise', 'Record', 'Partial', 'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable', 'ReturnType', 'Parameters', 'InstanceType'],
123
+ java: ['int', 'long', 'short', 'byte', 'float', 'double', 'char', 'boolean', 'String', 'Integer', 'Long', 'Double', 'Float', 'Boolean', 'Object', 'List', 'Map', 'Set', 'ArrayList', 'HashMap', 'HashSet'],
124
+ go: ['int', 'int8', 'int16', 'int32', 'int64', 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'float32', 'float64', 'complex64', 'complex128', 'byte', 'rune', 'string', 'bool', 'error'],
125
+ rust: ['i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize', 'f32', 'f64', 'bool', 'char', 'str', 'String', 'Vec', 'Option', 'Result', 'Box', 'Rc', 'Arc', 'Cell', 'RefCell']
126
+ };
127
+
128
+ // 获取语言类型
129
+ function getLangType(lang) {
130
+ const l = (lang || '').toLowerCase();
131
+ if (['js', 'javascript', 'jsx'].includes(l)) return 'js';
132
+ if (['ts', 'typescript', 'tsx'].includes(l)) return 'ts';
133
+ if (['py', 'python'].includes(l)) return 'python';
134
+ if (['java', 'kt', 'kotlin'].includes(l)) return 'java';
135
+ if (['go', 'golang'].includes(l)) return 'go';
136
+ if (['rs', 'rust'].includes(l)) return 'rust';
137
+ if (['c', 'cpp', 'c++', 'h', 'hpp'].includes(l)) return 'cpp';
138
+ if (['sql', 'mysql', 'postgresql', 'sqlite'].includes(l)) return 'sql';
139
+ if (['html', 'htm', 'xml'].includes(l)) return 'html';
140
+ if (['css', 'scss', 'sass', 'less'].includes(l)) return 'css';
141
+ if (['json', 'jsonc'].includes(l)) return 'json';
142
+ if (['sh', 'bash', 'shell', 'zsh'].includes(l)) return 'shell';
143
+ if (['vue'].includes(l)) return 'vue';
144
+ return 'js'; // 默认
145
+ }
146
+
147
+ // 转义 HTML(只转义内容,不转义标签)
148
+ function escapeHtml(str) {
149
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
150
+ }
151
+
152
+ // 高亮颜色配置
153
+ const darkColors = {
154
+ keyword: '#c586c0',
155
+ string: '#ce9178',
156
+ number: '#b5cea8',
157
+ comment: '#6a9955',
158
+ boolean: '#569cd6',
159
+ function: '#dcdcaa',
160
+ type: '#4ec9b0',
161
+ property: '#4fc1ff',
162
+ tag: '#569cd6',
163
+ attr: '#9cdcfe',
164
+ selector: '#d7ba7d',
165
+ decorator: '#dcdcaa',
166
+ operator: '#d4d4d4',
167
+ bracket: '#ffd700'
168
+ };
169
+
170
+ const lightColors = {
171
+ keyword: '#af00db',
172
+ string: '#a31515',
173
+ number: '#098658',
174
+ comment: '#008000',
175
+ boolean: '#0000ff',
176
+ function: '#795e26',
177
+ type: '#267f99',
178
+ property: '#001080',
179
+ tag: '#800000',
180
+ attr: '#e50000',
181
+ selector: '#800000',
182
+ decorator: '#795e26',
183
+ operator: '#000000',
184
+ bracket: '#0431fa'
185
+ };
186
+
187
+ // 获取当前主题的颜色
188
+ const hlColors = computed(() => {
189
+ return actualTheme.value === 'dark' ? darkColors : lightColors;
190
+ });
191
+
192
+ // 包裹高亮
193
+ function wrap(color, text) {
194
+ return `<span style="color:${color}">${escapeHtml(text)}</span>`;
195
+ }
196
+
197
+ // 语法高亮处理
198
+ function highlightLine(line) {
199
+ if (!props.highlight) return escapeHtml(line);
200
+
201
+ const langType = getLangType(props.language);
202
+
203
+ // 使用 token 方式解析
204
+ return tokenize(line, langType);
205
+ }
206
+
207
+ // 简单的词法分析
208
+ function tokenize(line, langType) {
209
+ const tokens = [];
210
+ let i = 0;
211
+ const colors = hlColors.value;
212
+
213
+ while (i < line.length) {
214
+ let matched = false;
215
+
216
+ // 字符串 - 双引号
217
+ if (line[i] === '"') {
218
+ const end = findStringEnd(line, i, '"');
219
+ tokens.push(wrap(colors.string, line.slice(i, end)));
220
+ i = end;
221
+ matched = true;
222
+ }
223
+ // 字符串 - 单引号
224
+ else if (line[i] === "'") {
225
+ const end = findStringEnd(line, i, "'");
226
+ tokens.push(wrap(colors.string, line.slice(i, end)));
227
+ i = end;
228
+ matched = true;
229
+ }
230
+ // 字符串 - 模板字符串
231
+ else if (line[i] === '`') {
232
+ const end = findStringEnd(line, i, '`');
233
+ tokens.push(wrap(colors.string, line.slice(i, end)));
234
+ i = end;
235
+ matched = true;
236
+ }
237
+ // 单行注释
238
+ else if (line.slice(i, i + 2) === '//') {
239
+ tokens.push(wrap(colors.comment, line.slice(i)));
240
+ break;
241
+ }
242
+ // Python/Shell 注释
243
+ else if (['python', 'shell'].includes(langType) && line[i] === '#') {
244
+ tokens.push(wrap(colors.comment, line.slice(i)));
245
+ break;
246
+ }
247
+ // 数字
248
+ else if (/\d/.test(line[i]) && (i === 0 || !/\w/.test(line[i - 1]))) {
249
+ const match = line.slice(i).match(/^(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)/);
250
+ if (match) {
251
+ tokens.push(wrap(colors.number, match[0]));
252
+ i += match[0].length;
253
+ matched = true;
254
+ }
255
+ }
256
+ // 标识符和关键字
257
+ else if (/[a-zA-Z_]/.test(line[i])) {
258
+ const match = line.slice(i).match(/^[a-zA-Z_]\w*/);
259
+ if (match) {
260
+ const word = match[0];
261
+ const nextChar = line[i + word.length] || '';
262
+ const keywords = langKeywords[langType] || langKeywords.js;
263
+ const types = typeKeywords[langType] || [];
264
+
265
+ if (keywords.includes(word)) {
266
+ tokens.push(wrap(colors.keyword, word));
267
+ } else if (types.includes(word)) {
268
+ tokens.push(wrap(colors.type, word));
269
+ } else if (['true', 'false', 'null', 'undefined', 'NaN', 'Infinity', 'True', 'False', 'None', 'nil'].includes(word)) {
270
+ tokens.push(wrap(colors.boolean, word));
271
+ } else if (nextChar === '(') {
272
+ tokens.push(wrap(colors.function, word));
273
+ } else if (nextChar === ':') {
274
+ tokens.push(wrap(colors.property, word));
275
+ } else {
276
+ tokens.push(escapeHtml(word));
277
+ }
278
+ i += word.length;
279
+ matched = true;
280
+ }
281
+ }
282
+ // 括号
283
+ else if (/[(){}[\]]/.test(line[i])) {
284
+ tokens.push(wrap(colors.bracket, line[i]));
285
+ i++;
286
+ matched = true;
287
+ }
288
+ // 装饰器
289
+ else if (line[i] === '@' && ['python', 'java', 'ts'].includes(langType)) {
290
+ const match = line.slice(i).match(/^@\w+/);
291
+ if (match) {
292
+ tokens.push(wrap(colors.decorator, match[0]));
293
+ i += match[0].length;
294
+ matched = true;
295
+ }
296
+ }
297
+
298
+ // 其他字符
299
+ if (!matched) {
300
+ tokens.push(escapeHtml(line[i]));
301
+ i++;
302
+ }
303
+ }
304
+
305
+ return tokens.join('');
306
+ }
307
+
308
+ // 查找字符串结束位置
309
+ function findStringEnd(line, start, quote) {
310
+ let i = start + 1;
311
+ while (i < line.length) {
312
+ if (line[i] === '\\') {
313
+ i += 2;
314
+ } else if (line[i] === quote) {
315
+ return i + 1;
316
+ } else {
317
+ i++;
318
+ }
319
+ }
320
+ return line.length;
321
+ }
322
+
323
+ // JSON 高亮
324
+ function highlightJson(line) {
325
+ let result = line;
326
+ // 键名
327
+ result = result.replace(/(&quot;)([^&]+?)(&quot;)\s*:/g, `<span style="color:${hlColors.property}">$1$2$3</span>:`);
328
+ // 字符串值
329
+ result = result.replace(/:(\s*)(&quot;)([^&]*?)(&quot;)/g, `:$1<span style="color:${hlColors.string}">$2$3$4</span>`);
330
+ // 数字
331
+ result = result.replace(/:\s*(-?\d+\.?\d*)/g, `: <span style="color:${hlColors.number}">$1</span>`);
332
+ // 布尔和 null
333
+ result = result.replace(/:\s*(true|false|null)\b/g, `: <span style="color:${hlColors.boolean}">$1</span>`);
334
+ return result;
335
+ }
336
+
337
+ // HTML 高亮
338
+ function highlightHtml(line) {
339
+ let result = line;
340
+ // 标签
341
+ result = result.replace(/(&lt;\/?)([\w-]+)/g, `$1<span style="color:${hlColors.tag}">$2</span>`);
342
+ // 属性名
343
+ result = result.replace(/\s([\w-]+)(=)/g, ` <span style="color:${hlColors.attr}">$1</span>$2`);
344
+ // 属性值
345
+ result = result.replace(/(=)(&quot;[^&]*?&quot;|'[^']*?')/g, `$1<span style="color:${hlColors.string}">$2</span>`);
346
+ // 注释
347
+ result = result.replace(/(&lt;!--.*?--&gt;)/g, `<span style="color:${hlColors.comment}">$1</span>`);
348
+ return result;
349
+ }
350
+
351
+ // CSS 高亮
352
+ function highlightCss(line) {
353
+ let result = line;
354
+ // 选择器(简单匹配)
355
+ result = result.replace(/^(\s*)([\.\#\w\-\[\]=\"\'\:\s,>+~]+)(\s*\{)/g, `$1<span style="color:${hlColors.selector}">$2</span>$3`);
356
+ // 属性名
357
+ result = result.replace(/([\w-]+)(\s*:)/g, `<span style="color:${hlColors.property}">$1</span>$2`);
358
+ // 属性值中的颜色
359
+ result = result.replace(/(#[0-9a-fA-F]{3,8})\b/g, `<span style="color:${hlColors.string}">$1</span>`);
360
+ // 数字和单位
361
+ result = result.replace(/\b(-?\d+\.?\d*)(px|em|rem|%|vh|vw|deg|s|ms)?\b/g, `<span style="color:${hlColors.number}">$1$2</span>`);
362
+ // 注释
363
+ result = result.replace(/(\/\*.*?\*\/)/g, `<span style="color:${hlColors.comment}">$1</span>`);
364
+ // !important
365
+ result = result.replace(/(!important)/gi, `<span style="color:${hlColors.keyword}">$1</span>`);
366
+ return result;
367
+ }
368
+
369
+ // SQL 高亮
370
+ function highlightSql(line) {
371
+ let result = line;
372
+ const keywords = langKeywords.sql;
373
+ keywords.forEach(kw => {
374
+ const regex = new RegExp(`\\b(${kw})\\b`, 'gi');
375
+ result = result.replace(regex, `<span style="color:${hlColors.keyword}">$1</span>`);
376
+ });
377
+ // 字符串
378
+ result = result.replace(/('[^']*')/g, `<span style="color:${hlColors.string}">$1</span>`);
379
+ // 数字
380
+ result = result.replace(/\b(\d+\.?\d*)\b/g, `<span style="color:${hlColors.number}">$1</span>`);
381
+ // 注释
382
+ result = result.replace(/(--.*$)/g, `<span style="color:${hlColors.comment}">$1</span>`);
383
+ return result;
384
+ }
385
+
386
+ // 通用高亮
387
+ function highlightGeneral(line, langType) {
388
+ let result = line;
389
+
390
+ // 字符串(需要先处理,避免被其他规则干扰)
391
+ result = result.replace(/(&quot;)([^&]*?)(&quot;)/g, `<span style="color:${hlColors.string}">$1$2$3</span>`);
392
+ result = result.replace(/('[^'\\]*(?:\\.[^'\\]*)*')/g, `<span style="color:${hlColors.string}">$1</span>`);
393
+ result = result.replace(/(`[^`\\]*(?:\\.[^`\\]*)*`)/g, `<span style="color:${hlColors.string}">$1</span>`);
394
+
395
+ // 注释
396
+ if (['python', 'shell'].includes(langType)) {
397
+ result = result.replace(/(#.*$)/g, `<span style="color:${hlColors.comment}">$1</span>`);
398
+ } else {
399
+ result = result.replace(/(\/\/.*$)/g, `<span style="color:${hlColors.comment}">$1</span>`);
400
+ result = result.replace(/(\/\*.*?\*\/)/g, `<span style="color:${hlColors.comment}">$1</span>`);
401
+ }
402
+
403
+ // 类型关键字
404
+ const types = typeKeywords[langType] || [];
405
+ if (types.length > 0) {
406
+ const typeRegex = new RegExp(`\\b(${types.join('|')})\\b`, 'g');
407
+ result = result.replace(typeRegex, `<span style="color:${hlColors.type}">$1</span>`);
408
+ }
409
+
410
+ // 语言关键字
411
+ const keywords = langKeywords[langType] || langKeywords.js;
412
+ if (keywords.length > 0) {
413
+ const kwRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
414
+ result = result.replace(kwRegex, `<span style="color:${hlColors.keyword}">$1</span>`);
415
+ }
416
+
417
+ // 函数调用
418
+ result = result.replace(/\b([a-zA-Z_]\w*)\s*\(/g, `<span style="color:${hlColors.function}">$1</span>(`);
419
+
420
+ // 对象属性名 (key: value 格式)
421
+ result = result.replace(/\b([a-zA-Z_]\w*)\s*:/g, `<span style="color:${hlColors.property}">$1</span>:`);
422
+
423
+ // 括号高亮
424
+ result = result.replace(/([(){}[\]])/g, `<span style="color:${hlColors.bracket}">$1</span>`);
425
+
426
+ // 数字(十六进制、二进制、八进制、浮点数、整数)
427
+ result = result.replace(/\b(0x[0-9a-fA-F]+|0b[01]+|0o[0-7]+|\d+\.?\d*(?:e[+-]?\d+)?)\b/g, `<span style="color:${hlColors.number}">$1</span>`);
428
+
429
+ // 布尔值和特殊值
430
+ if (['python'].includes(langType)) {
431
+ result = result.replace(/\b(True|False|None)\b/g, `<span style="color:${hlColors.boolean}">$1</span>`);
432
+ } else if (['rust'].includes(langType)) {
433
+ result = result.replace(/\b(true|false|Some|None|Ok|Err)\b/g, `<span style="color:${hlColors.boolean}">$1</span>`);
434
+ } else if (['go'].includes(langType)) {
435
+ result = result.replace(/\b(true|false|nil|iota)\b/g, `<span style="color:${hlColors.boolean}">$1</span>`);
436
+ } else {
437
+ result = result.replace(/\b(true|false|null|undefined|NaN|Infinity)\b/g, `<span style="color:${hlColors.boolean}">$1</span>`);
438
+ }
439
+
440
+ // 装饰器/注解
441
+ if (['python', 'java', 'ts'].includes(langType)) {
442
+ result = result.replace(/(@\w+)/g, `<span style="color:${hlColors.decorator}">$1</span>`);
443
+ }
444
+
445
+ // 操作符
446
+ result = result.replace(/(\+\+|--|&&|\|\||===|!==|==|!=|&lt;=|&gt;=|=&gt;|\+=|-=|\*=|\/=)/g, `<span style="color:${hlColors.operator}">$1</span>`);
447
+
448
+ return result;
449
+ }
450
+
451
+ // 解析括号折叠区域
452
+ const foldRanges = computed(() => {
453
+ if (!props.foldable) return [];
454
+ const ranges = [];
455
+ const stack = [];
456
+ const lines = codeLines.value;
457
+
458
+ lines.forEach((line, index) => {
459
+ const openCount = (line.match(/[\{\[\(]/g) || []).length;
460
+ const closeCount = (line.match(/[\}\]\)]/g) || []).length;
461
+
462
+ for (let i = 0; i < openCount; i++) {
463
+ stack.push(index);
464
+ }
465
+ for (let i = 0; i < closeCount; i++) {
466
+ if (stack.length > 0) {
467
+ const start = stack.pop();
468
+ if (index > start) {
469
+ ranges.push({ start, end: index });
470
+ }
471
+ }
472
+ }
473
+ });
474
+ return ranges;
475
+ });
476
+
477
+ // 计算显示的行
478
+ const displayLines = computed(() => {
479
+ const lines = codeLines.value;
480
+ const result = [];
481
+
482
+ lines.forEach((content, index) => {
483
+ const foldRange = foldRanges.value.find(r => r.start === index);
484
+ const isFolded = foldedRanges.value.has(index);
485
+ const isHidden = Array.from(foldedRanges.value).some(start => {
486
+ const range = foldRanges.value.find(r => r.start === start);
487
+ return range && index > range.start && index <= range.end;
488
+ });
489
+
490
+ result.push({
491
+ lineNum: index + 1,
492
+ content,
493
+ htmlContent: highlightLine(content),
494
+ visible: !isHidden,
495
+ foldable: !!foldRange,
496
+ foldStart: index,
497
+ folded: isFolded
498
+ });
499
+ });
500
+ return result;
501
+ });
502
+
503
+ // 切换括号折叠
504
+ function toggleFold(lineIndex) {
505
+ if (foldedRanges.value.has(lineIndex)) {
506
+ foldedRanges.value.delete(lineIndex);
507
+ } else {
508
+ foldedRanges.value.add(lineIndex);
509
+ }
510
+ foldedRanges.value = new Set(foldedRanges.value);
511
+ }
512
+
513
+ function handleCopy() {
514
+ uni.setClipboardData({
515
+ data: props.code,
516
+ success: () => {
517
+ copied.value = true;
518
+ setTimeout(() => { copied.value = false; }, 2000);
519
+ }
520
+ });
521
+ }
522
+
523
+ function toggleExpand() {
524
+ isExpanded.value = !isExpanded.value;
525
+ }
526
+ </script>
527
+
528
+ <style lang="scss">
529
+ .prism-code {
530
+ border-radius: 12rpx;
531
+ overflow: hidden;
532
+ margin: 10rpx auto;
533
+ }
534
+
535
+ /* 头部 */
536
+ .code-header {
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: space-between;
540
+ padding: 16rpx 20rpx;
541
+ border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
542
+ }
543
+
544
+ .header-left {
545
+ display: flex;
546
+ align-items: center;
547
+ gap: 12rpx;
548
+ }
549
+
550
+ .header-right {
551
+ display: flex;
552
+ align-items: center;
553
+ gap: 12rpx;
554
+ }
555
+
556
+ .code-expand {
557
+ width: 40rpx;
558
+ height: 40rpx;
559
+ display: flex;
560
+ align-items: center;
561
+ justify-content: center;
562
+ cursor: pointer;
563
+
564
+ .fa { font-size: 22rpx; }
565
+ &:active { opacity: 0.7; }
566
+ }
567
+
568
+ .code-lang {
569
+ font-size: 22rpx;
570
+ font-weight: 600;
571
+ text-transform: uppercase;
572
+ }
573
+
574
+ .code-copy {
575
+ display: flex;
576
+ align-items: center;
577
+ gap: 8rpx;
578
+ padding: 8rpx 16rpx;
579
+ border-radius: 8rpx;
580
+ transition: all 0.2s;
581
+
582
+ &:active { opacity: 0.7; }
583
+
584
+ .fa { font-size: 24rpx; }
585
+ .copy-text { font-size: 22rpx; }
586
+ }
587
+
588
+ /* 代码内容 */
589
+ .code-body {
590
+ padding: 20rpx;
591
+ overflow-x: auto;
592
+
593
+ /* 自定义滚动条 - 默认隐藏,悬停显示 */
594
+ &::-webkit-scrollbar {
595
+ height: 4px;
596
+ background: transparent;
597
+ }
598
+
599
+ &::-webkit-scrollbar-thumb {
600
+ background: transparent;
601
+ border-radius: 2px;
602
+ }
603
+
604
+ &:hover::-webkit-scrollbar-thumb {
605
+ background: rgba(128, 128, 128, 0.4);
606
+ }
607
+
608
+ &::-webkit-scrollbar-thumb:hover {
609
+ background: rgba(128, 128, 128, 0.6);
610
+ }
611
+ }
612
+
613
+ .code-lines {
614
+ display: flex;
615
+ flex-direction: column;
616
+ }
617
+
618
+ .code-line {
619
+ display: flex;
620
+ line-height: 1.6;
621
+ font-family: 'Courier New', Consolas, Monaco, monospace;
622
+ font-size: 24rpx;
623
+ }
624
+
625
+ /* 括号折叠按钮 */
626
+ .fold-btn {
627
+ width: 28rpx;
628
+ height: 28rpx;
629
+ display: flex;
630
+ align-items: center;
631
+ justify-content: center;
632
+ cursor: pointer;
633
+ flex-shrink: 0;
634
+ margin-right: 8rpx;
635
+ border-radius: 4rpx;
636
+
637
+ .fa { font-size: 16rpx; }
638
+ &:active { opacity: 0.7; }
639
+ }
640
+
641
+ .fold-placeholder {
642
+ width: 28rpx;
643
+ margin-right: 8rpx;
644
+ flex-shrink: 0;
645
+ }
646
+
647
+ .line-number {
648
+ min-width: 48rpx;
649
+ padding-right: 16rpx;
650
+ text-align: right;
651
+ user-select: none;
652
+ opacity: 0.5;
653
+ }
654
+
655
+ .line-content {
656
+ flex: 1;
657
+ white-space: pre-wrap;
658
+ word-break: break-all;
659
+ }
660
+
661
+ /* 语法高亮颜色 */
662
+ .hl-keyword { color: #c586c0; }
663
+ .hl-string { color: #ce9178; }
664
+ .hl-number { color: #b5cea8; }
665
+ .hl-comment { color: #6a9955; }
666
+ .hl-boolean { color: #569cd6; }
667
+ .hl-function { color: #dcdcaa; }
668
+ .hl-type { color: #4ec9b0; }
669
+ .hl-property { color: #4fc1ff; }
670
+ .hl-tag { color: #569cd6; }
671
+ .hl-attr { color: #9cdcfe; }
672
+ .hl-selector { color: #d7ba7d; }
673
+ .hl-color { color: #ce9178; }
674
+ .hl-decorator { color: #dcdcaa; }
675
+ .hl-operator { color: #d4d4d4; }
676
+ .hl-bracket { color: #ffd700; }
677
+
678
+ /* 深色主题(默认) */
679
+ .prism-code.dark {
680
+ background: #1e1e1e;
681
+
682
+ .code-header {
683
+ background: #2d2d2d;
684
+ border-bottom: 1rpx solid #404040;
685
+ }
686
+
687
+ .code-lang { color: #9cdcfe; }
688
+
689
+ .code-copy {
690
+ color: #808080;
691
+ background: rgba(255, 255, 255, 0.05);
692
+ &:hover { background: rgba(255, 255, 255, 0.1); }
693
+ }
694
+
695
+ .line-number { color: #858585; }
696
+ .line-content { color: #d4d4d4; }
697
+ .code-expand .fa { color: #808080; }
698
+ .fold-btn {
699
+ color: #808080;
700
+ background: rgba(255, 255, 255, 0.05);
701
+ &:hover { background: rgba(255, 255, 255, 0.1); }
702
+ }
703
+ }
704
+
705
+ /* 浅色主题 */
706
+ .prism-code.light {
707
+ background: #f5f5f5;
708
+ border: 1rpx solid #e0e0e0;
709
+
710
+ .code-header {
711
+ background: #ebebeb;
712
+ border-bottom-color: #e0e0e0;
713
+ }
714
+
715
+ .code-lang { color: #666666; }
716
+
717
+ .code-copy {
718
+ color: #666666;
719
+ background: rgba(0, 0, 0, 0.05);
720
+ &:hover { background: rgba(0, 0, 0, 0.1); }
721
+ }
722
+
723
+ .line-number { color: #999999; }
724
+ .line-content { color: #333333; }
725
+ .code-expand .fa { color: #666666; }
726
+ .fold-btn {
727
+ color: #666666;
728
+ background: rgba(0, 0, 0, 0.05);
729
+ &:hover { background: rgba(0, 0, 0, 0.1); }
730
+ }
731
+
732
+ /* 浅色主题高亮颜色 */
733
+ .hl-keyword { color: #af00db; }
734
+ .hl-string { color: #a31515; }
735
+ .hl-number { color: #098658; }
736
+ .hl-comment { color: #008000; }
737
+ .hl-boolean { color: #0000ff; }
738
+ .hl-function { color: #795e26; }
739
+ .hl-type { color: #267f99; }
740
+ .hl-property { color: #001080; }
741
+ .hl-tag { color: #800000; }
742
+ .hl-attr { color: #e50000; }
743
+ .hl-selector { color: #800000; }
744
+ .hl-color { color: #a31515; }
745
+ .hl-decorator { color: #795e26; }
746
+ .hl-operator { color: #000000; }
747
+ .hl-bracket { color: #0431fa; }
748
+ }
749
+ </style>