vuepress-theme-uniapp-official 1.6.10 → 1.6.11

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.
@@ -13,10 +13,9 @@
13
13
  <div class="meta">
14
14
  <span class="time">{{ m.time }}</span>
15
15
 
16
- <!-- <div class="actions" v-if="m.role === 'assistant'">
17
- <LikeButton :active="!!m.like" type="like" @click.stop="like(m)" />
18
- <LikeButton :active="!!m.dislike" type="dislike" @click.stop="dislike(m)" />
19
- </div> -->
16
+ <div class="actions" v-if="m.role === 'assistant' && m.uni_ai_feedback_id.length > 0">
17
+ <AIFeedback :like="m.like" :dislike="m.dislike" :uni_ai_feedback_id="m.uni_ai_feedback_id" @action="data => feedbackAction(m.id, data)"/>
18
+ </div>
20
19
  </div>
21
20
 
22
21
  </div>
@@ -59,13 +58,13 @@
59
58
  import { ref, nextTick, watchEffect, onMounted, computed } from 'vue'
60
59
  import searchPageConfig from '@theme-config/searchPage';
61
60
  import { renderMarkdown } from "./markdown-loader";
62
- import { MAX_AI_ANSWER_LENGTH, AI_CHAT_FOR_DOC_SEARCH_URL } from '../constants';
61
+ import { MAX_AI_ANSWER_LENGTH, AI_CHAT_FOR_DOC_SEARCH_URL, AI_UPDATE_FEEDBACK_URL } from '../constants';
63
62
  import { ajax } from '../utils/postDcloudServer';
64
63
  import SelectPlatform from '../components/SelectPlatform.vue';
65
- import LikeButton from '../components/LikeButton.vue';
64
+ import AIFeedback from '../components/AIFeedback.vue';
66
65
  import Skeleton from '../components/Skeleton.vue';
67
66
 
68
- const { aiPlatforms = [], aiChatForDocSearch = AI_CHAT_FOR_DOC_SEARCH_URL } = searchPageConfig;
67
+ const { aiPlatforms = [], aiChatForDocSearch = AI_CHAT_FOR_DOC_SEARCH_URL, aiUpdateFeedbackUrl = AI_UPDATE_FEEDBACK_URL } = searchPageConfig;
69
68
 
70
69
  const props = defineProps({
71
70
  currentCategory: {
@@ -232,6 +231,7 @@ async function send() {
232
231
  sending.value = true
233
232
 
234
233
  let fakeReply = ''
234
+ let uni_ai_feedback_id = ''
235
235
  try {
236
236
  const res = await ajax(aiChatForDocSearch, 'POST', {
237
237
  "question": userText,
@@ -240,6 +240,7 @@ async function send() {
240
240
  })
241
241
  if (res.errorCode === 0) {
242
242
  fakeReply = res.chunk
243
+ uni_ai_feedback_id = res.uni_ai_feedback_id
243
244
  } else {
244
245
  fakeReply = `抱歉,AI 助手出错了:${res.errorMsg || '未知错误'}`
245
246
  }
@@ -253,6 +254,7 @@ async function send() {
253
254
  id: Date.now() + 1,
254
255
  role: 'assistant',
255
256
  raw: fakeReply,
257
+ uni_ai_feedback_id, // 从接口获取反馈 ID
256
258
  rendered: await renderHTML(fakeReply),
257
259
  time: formatTime(),
258
260
  isTyping: false,
@@ -269,19 +271,13 @@ async function send() {
269
271
  scrollToBottom()
270
272
  }
271
273
 
272
- function like(m) {
273
- console.log('like');
274
- m.like = !m.like;
275
- if (m.like) {
276
- m.dislike = false;
277
- }
278
- }
279
-
280
- function dislike(m) {
281
- console.log('dislike');
282
- m.dislike = !m.dislike;
283
- if (m.dislike) {
284
- m.like = false;
274
+ function feedbackAction(messageId, data) {
275
+ // 更新对应消息的反馈状态
276
+ const msg = messages.value.find(m => m.id === messageId);
277
+ if (msg) {
278
+ msg.like = data.like;
279
+ msg.dislike = data.dislike;
280
+ setSessionMessages(messages.value)
285
281
  }
286
282
  }
287
283
 
@@ -13,23 +13,17 @@
13
13
  本回答由 AI 生成,可能已过期、失效或不适用于当前情形,仅供参考
14
14
  </div>
15
15
 
16
- <!-- <div v-show="hasMessage" class="ai-answer-footer_right">
17
- <LikeButton :active="status.like" type="like" @click.stop="like" />
18
- <LikeButton :active="status.dislike" type="dislike" @click.stop="dislike" />
19
- </div> -->
16
+ <div v-show="hasMessage && props.item.uni_ai_feedback_id.length > 0">
17
+ <AIFeedback :uni_ai_feedback_id="props.item.uni_ai_feedback_id"/>
18
+ </div>
20
19
  </div>
21
20
  </div>
22
21
  </template>
23
22
 
24
23
  <script setup>
25
- import searchPageConfig from '@theme-config/searchPage';
24
+ import { computed } from 'vue';
26
25
  import Skeleton from './Skeleton.vue';
27
- import LikeButton from './LikeButton.vue';
28
- import { computed, reactive } from 'vue';
29
-
30
- const {
31
- aiChatForDocSearch = 'https://ai-assist-api.dcloud.net.cn/tbox/chatForDocSearch'
32
- } = searchPageConfig;
26
+ import AIFeedback from './AIFeedback.vue';
33
27
 
34
28
  const props = defineProps({
35
29
  item: {
@@ -41,27 +35,6 @@ const props = defineProps({
41
35
  const hasMessage = computed(() => {
42
36
  return props.item.msg && props.item.msg.length > 0;
43
37
  })
44
-
45
- const status = reactive({
46
- like: false,
47
- dislike: false
48
- })
49
-
50
- function like() {
51
- console.log('like');
52
- status.like = !status.like;
53
- if (status.like) {
54
- status.dislike = false;
55
- }
56
- }
57
-
58
- function dislike() {
59
- console.log('dislike');
60
- status.dislike = !status.dislike;
61
- if (status.dislike) {
62
- status.like = false;
63
- }
64
- }
65
38
  </script>
66
39
 
67
40
  <style lang="stylus">
@@ -122,7 +95,4 @@ function dislike() {
122
95
  justify-content space-between
123
96
  font-size 12px
124
97
  color #888
125
-
126
- .ai-answer-footer_right
127
- display flex
128
98
  </style>
@@ -0,0 +1,246 @@
1
+ <script setup>
2
+ import { reactive, computed, ref } from 'vue';
3
+ import searchPageConfig from '@theme-config/searchPage';
4
+ import LikeButton from './LikeButton.vue';
5
+ import { sendFeedback } from '../utils/aiUtils';
6
+ import { AI_UPDATE_FEEDBACK_URL } from '../constants';
7
+ import { usePopoverDirective } from '../../../util'
8
+
9
+ const { aiUpdateFeedbackUrl = AI_UPDATE_FEEDBACK_URL, } = searchPageConfig;
10
+ const reasons = [
11
+ { label: '字数太长' },
12
+ { label: '常识错误' },
13
+ { label: '与提问不符' },
14
+ { label: '产品体验差' }
15
+ ];
16
+ const { mouseEnter, directive: vPopover } = usePopoverDirective('#search-container')
17
+
18
+ const props = defineProps({
19
+ uni_ai_feedback_id: {
20
+ type: String,
21
+ default: ''
22
+ },
23
+ like: {
24
+ type: Boolean,
25
+ default: false
26
+ },
27
+ dislike: {
28
+ type: Boolean,
29
+ default: false
30
+ }
31
+ })
32
+ const emit = defineEmits(['action'])
33
+
34
+ const status = reactive({
35
+ like: props.like,
36
+ dislike: props.dislike
37
+ })
38
+ const selectedReasons = ref([]);
39
+ const comment = ref('');
40
+ const aiFeedbackVisible = ref(false);
41
+ const feedbackStatus = computed(() => {
42
+ if (!status.dislike && !status.like) {
43
+ return ''
44
+ }
45
+ if (status.like) {
46
+ return true
47
+ } else {
48
+ return false
49
+ }
50
+ })
51
+ const lastStatus = reactive({
52
+ like: false,
53
+ dislike: false
54
+ })
55
+
56
+ function setLastStatus() {
57
+ lastStatus.like = status.like;
58
+ lastStatus.dislike = status.dislike;
59
+ }
60
+ function resetStatus() {
61
+ status.like = lastStatus.like;
62
+ status.dislike = lastStatus.dislike;
63
+ }
64
+ function feedback() {
65
+ sendFeedback(aiUpdateFeedbackUrl, {
66
+ uni_ai_feedback_id: props.uni_ai_feedback_id,
67
+ comment: `${selectedReasons.value.join(';')} ${comment.value}`.trim(),
68
+ status: feedbackStatus.value
69
+ })
70
+ .then((response) => {
71
+ emit('action', status)
72
+ if (!response) {
73
+ resetStatus()
74
+ }
75
+ })
76
+ .catch(() => {
77
+ resetStatus()
78
+ })
79
+ }
80
+
81
+ function like() {
82
+ if (props.uni_ai_feedback_id) {
83
+ setLastStatus()
84
+ status.like = !status.like;
85
+ if (status.like) {
86
+ status.dislike = false;
87
+ }
88
+ feedback()
89
+ }
90
+ }
91
+
92
+ function dislike() {
93
+ if (props.uni_ai_feedback_id) {
94
+ setLastStatus()
95
+ status.dislike = !status.dislike;
96
+ aiFeedbackVisible.value = status.dislike
97
+ if (status.dislike) {
98
+ status.like = false;
99
+ }
100
+ feedback()
101
+ }
102
+ }
103
+
104
+ function toggleReason(text) {
105
+ const index = selectedReasons.value.indexOf(text);
106
+ if (index > -1) {
107
+ selectedReasons.value.splice(index, 1);
108
+ } else {
109
+ selectedReasons.value.push(text);
110
+ }
111
+ }
112
+
113
+ function closeComment() {
114
+ selectedReasons.value = [];
115
+ comment.value = '';
116
+ aiFeedbackVisible.value = false
117
+ }
118
+ function sendComment() {
119
+ feedback()
120
+ if (aiFeedbackVisible.value) {
121
+ selectedReasons.value = [];
122
+ comment.value = '';
123
+ aiFeedbackVisible.value = false;
124
+ }
125
+ }
126
+ </script>
127
+
128
+ <template>
129
+ <div style="display: flex;">
130
+ <LikeButton :active="status.like" type="like" @click.stop="like" />
131
+ <div class="dislike_container" @mouseenter="mouseEnter">
132
+ <LikeButton :active="status.dislike" type="dislike" @click="dislike" />
133
+
134
+ <div class="ai-feedback" v-show="aiFeedbackVisible" v-popover>
135
+ <div class="ai-feedback-header">进一步反馈</div>
136
+
137
+ <div class="tags">
138
+ <div v-for="item in reasons" :key="item.label" class="tag"
139
+ :class="{ active: selectedReasons.includes(item.label) }" @click="toggleReason(item.label)">
140
+ {{ item.label }}
141
+ </div>
142
+ </div>
143
+
144
+ <textarea v-model="comment" class="feedback-input" placeholder="具体原因和建议"></textarea>
145
+
146
+ <div class="footer">
147
+ <button class="btn cancel" @click="closeComment">关闭</button>
148
+ <button class="btn submit" @click="sendComment">提交</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </template>
154
+
155
+ <style lang="stylus" scoped>
156
+ .ai-feedback
157
+ position absolute
158
+ width 300px
159
+ background #fff
160
+ border-radius 14px
161
+ padding 18px
162
+ box-shadow 0 8px 28px rgba(0,0,0,0.12)
163
+ border 1px solid rgba(0,0,0,0.06)
164
+ animation fadeIn .2s ease
165
+ box-sizing border-box
166
+
167
+ .ai-feedback-header
168
+ font-size 16px
169
+ font-weight 600
170
+ margin-bottom 14px
171
+ color #222
172
+
173
+ .tags
174
+ width 100%
175
+ display flex
176
+ flex-wrap wrap
177
+ justify-content space-between
178
+
179
+ .tag
180
+ width calc(50% - 8px)
181
+ background #f2f3f5
182
+ border-radius 8px
183
+ padding 8px 0
184
+ margin-bottom 8px
185
+ text-align center
186
+ font-size 14px
187
+ color #444
188
+ cursor pointer
189
+ transition all .2s ease
190
+
191
+ .tag.active
192
+ background $accentColor
193
+ color #fff
194
+
195
+ .feedback-input
196
+ width 100%
197
+ box-sizing border-box
198
+ padding 10px 12px
199
+ border 1px solid #e5e5e5
200
+ border-radius 10px
201
+ font-size 14px
202
+ color #333
203
+ min-height 60px
204
+ resize none
205
+ transition border .2s
206
+ outline none
207
+
208
+ &:focus
209
+ border 1px solid $accentColor
210
+
211
+ .footer
212
+ display flex
213
+ justify-content flex-end
214
+ margin-top 8px
215
+
216
+ .btn
217
+ padding 8px 16px
218
+ border-radius 8px
219
+ font-size 14px
220
+ border none
221
+ cursor pointer
222
+ transition background .2s
223
+
224
+ .cancel
225
+ background #f2f3f5
226
+ color #333
227
+
228
+ .cancel:hover
229
+ background #e5e6e7
230
+
231
+ .submit
232
+ margin-left 8px
233
+ background rgba($accentColor, .9)
234
+ color #fff
235
+
236
+ .submit:hover
237
+ background $accentColor
238
+
239
+ @keyframes fadeIn
240
+ from
241
+ opacity 0
242
+ transform translateY(-4px)
243
+ to
244
+ opacity 1
245
+ transform translateY(0)
246
+ </style>
@@ -1,4 +1,5 @@
1
1
  export const DEFAULT_ENABLE_AI = true; // 默认启用 AI 助手
2
2
  export const MAX_AI_ANSWER_LENGTH = 6; // AI 消息最小长度
3
3
  export const AI_CHAT_FOR_DOC_SEARCH_URL = 'https://ai-assist-api-p.dcloud.net.cn/http/tbox/chatForDocSearch'
4
+ export const AI_UPDATE_FEEDBACK_URL = 'https://ai-assist-api-p.dcloud.net.cn/http/tbox/updateFeedBackForDocSearch'
4
5
  export const AI_ERROR_MSG = '很抱歉,AI 助手未能找到相关答案。请尝试更换关键词进行搜索。'
@@ -159,7 +159,7 @@ const {
159
159
  resultsScreen: { resultsText, noResultsText, askNoResultsText },
160
160
  },
161
161
  extraFacetFilters = [],
162
- aiChatForDocSearch = AI_CHAT_FOR_DOC_SEARCH_URL
162
+ aiChatForDocSearch = AI_CHAT_FOR_DOC_SEARCH_URL,
163
163
  } = searchPageConfig;
164
164
  const crawlerUrl = 'https://zh.uniapp.dcloud.io/'
165
165
 
@@ -206,6 +206,7 @@ export default {
206
206
 
207
207
  searchAIResult: Promise.resolve(null),
208
208
  aiMessage: {
209
+ uni_ai_feedback_id: '',
209
210
  isAI: true,
210
211
  title: 'AI 助手回答',
211
212
  msg: ''
@@ -448,6 +449,7 @@ export default {
448
449
  "question": this.searchValue,
449
450
  "group_name": this.currentCategory.text
450
451
  }).then(res => {
452
+ this.aiMessage.uni_ai_feedback_id = res.uni_ai_feedback_id || ''
451
453
  if (res.errorCode === 0) {
452
454
  return renderMarkdown(res.chunk)
453
455
  } else {
@@ -0,0 +1,29 @@
1
+ import { ajax } from './postDcloudServer.js';
2
+
3
+ /**
4
+ *
5
+ * @param {string} url AI_UPDATE_FEEDBACK_URL
6
+ * @param {{ uni_ai_feedback_id: string, comment: string, status: boolean|string }} payload
7
+ *
8
+ * @returns {Promise<boolean>}
9
+ */
10
+ export function sendFeedback(url, payload) {
11
+ const data = {
12
+ uni_ai_feedback_id: payload.uni_ai_feedback_id,
13
+ comment: payload.comment,
14
+ status: payload.status, // true: like, false: dislike, '': neutral
15
+ };
16
+ return ajax(url, 'post', data)
17
+ .then(response => {
18
+ if (response.errorCode === 0) {
19
+ return true;
20
+ } else {
21
+ console.error('sendFeedback response error:', response.errorMsg);
22
+ return false;
23
+ }
24
+ })
25
+ .catch(error => {
26
+ console.error('sendFeedback error:', error);
27
+ return false;
28
+ });
29
+ }
@@ -1,27 +1,20 @@
1
1
  <script setup>
2
2
  import { ref, useSlots } from 'vue'
3
- import { getNavbarHeight } from '../util'
3
+ import { usePopoverDirective } from '../util'
4
4
 
5
5
  const emit = defineEmits(['show', 'hide'])
6
6
 
7
+ const { mouseEnter, directive: vPopover } = usePopoverDirective('.theme-default-content')
8
+
7
9
  const props = defineProps({
8
10
  table: String
9
11
  })
10
12
 
11
13
  const infoHover = ref(false)
12
14
  const slots = useSlots()
13
- let mouseOptions = {}
14
15
 
15
- const mouseEnter = (e) => {
16
- const { top, left, bottom, right, height, width } = e.target.getBoundingClientRect()
17
- mouseOptions = {
18
- top,
19
- left,
20
- bottom,
21
- right,
22
- height,
23
- width
24
- }
16
+ const wrapperMouseEnter = (e) => {
17
+ mouseEnter(e)
25
18
  infoHover.value = true
26
19
  emit('show')
27
20
  }
@@ -30,61 +23,12 @@ const mouseLeave = (e) => {
30
23
  infoHover.value = false
31
24
  emit('hide')
32
25
  }
33
-
34
- const vPopover = {
35
- inserted(el, binding, vnode, prevVnode) {
36
- const ThemeDefaultContent = document.querySelector('.theme-default-content')
37
- if (ThemeDefaultContent) {
38
- ThemeDefaultContent.appendChild(el)
39
- }
40
- },
41
- componentUpdated(el, binding, vnode, prevVnode) {
42
- const scrollTop = document.documentElement.scrollTop || document.body.scrollTop
43
- const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
44
- const screenWidth = document.documentElement.clientWidth
45
-
46
- const { width, height } = el.getBoundingClientRect()
47
- const { left: targetLeft, top: targetTop, bottom: targetBottom, right: targetRight, height: targetHeight, width: targetWidth } = mouseOptions
48
- const OFFSET = 10
49
- const NAVBAR_HEIGHT = getNavbarHeight()
50
-
51
- // 默认 icon 正中间位置
52
- const HALF_WIDTH = width / 2
53
- const HALF_TARGET_WIDTH = targetWidth / 2
54
- const TOP = height // + OFFSET
55
- const LEFT = HALF_TARGET_WIDTH - HALF_WIDTH
56
-
57
- const DEFAULT_TOP = targetTop + scrollTop
58
- const DEFAULT_LEFT = targetLeft + scrollLeft
59
-
60
- let currentTop = DEFAULT_TOP - TOP
61
- let currentLeft = DEFAULT_LEFT + LEFT
62
-
63
- if (targetTop - NAVBAR_HEIGHT - TOP - OFFSET > 0) {
64
- // 上方空间够
65
- currentTop = currentTop - OFFSET
66
- } else {
67
- // 上方空间不够
68
- currentTop = currentTop + (height + targetHeight) + OFFSET
69
- }
70
-
71
- const RIGHT_OFFSET = (screenWidth - (targetLeft + HALF_TARGET_WIDTH)) - HALF_WIDTH
72
- // const LEFT_OFFSET = (screenWidth - targetLeft - HALF_TARGET_WIDTH) - HALF_WIDTH
73
-
74
- if (RIGHT_OFFSET < 0) {
75
- // 右侧空间不够
76
- currentLeft = screenWidth - width
77
- }
78
- el.style.top = `${currentTop < 0 ? 0 : currentTop}px`
79
- el.style.left = `${currentLeft < 0 ? 0 : currentLeft}px`
80
- }
81
- }
82
26
  </script>
83
27
 
84
28
  <template>
85
29
  <div class="popover" @mouseleave="mouseLeave">
86
30
  <span>
87
- <div class="popover-reference-wrapper" @mouseenter="mouseEnter">
31
+ <div class="popover-reference-wrapper" @mouseenter="wrapperMouseEnter">
88
32
  <slot name="reference"></slot>
89
33
  <svg v-if="!slots.reference" width="20" height="20" t="1715917545486" class="icon" viewBox="0 0 1024 1024"
90
34
  version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10306" fill="#2c3e50">
@@ -113,7 +57,7 @@ const vPopover = {
113
57
  .popover-content-wrapper {
114
58
  position: absolute;
115
59
  background-color: #fff;
116
- z-index: 11;
60
+ z-index: 601;
117
61
  border-radius: 5px;
118
62
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
119
63
  transition: opacity .1s cubic-bezier(0.57, 0.85, 0.85, 0.57);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vuepress-theme-uniapp-official",
3
- "version": "1.6.10",
3
+ "version": "1.6.11",
4
4
  "description": "uni-app official website theme for vuepress",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -45,9 +45,9 @@
45
45
  "vuepress-plugin-mermaidjs": "1.9.1",
46
46
  "vuepress-plugin-named-chunks": "^1.1.4",
47
47
  "vuepress-plugin-zooming": "^1.1.8",
48
- "vuepress-plugin-expandable-row": "^1.0.10",
49
48
  "vuepress-plugin-noscript-code": "^1.0.2",
50
- "vuepress-plugin-check-md2": "^1.0.5"
49
+ "vuepress-plugin-check-md2": "^1.0.5",
50
+ "vuepress-plugin-expandable-row": "^1.0.10"
51
51
  },
52
52
  "resolutions": {
53
53
  "terser-webpack-plugin": "1.4.6",
package/util/index.js CHANGED
@@ -1,121 +1,119 @@
1
1
  import Vue from 'vue';
2
2
 
3
- export const isServer = Vue.prototype.$isServer
4
- export const hashRE = /#.*$/
5
- export const extRE = /\.(md|html)$/
6
- export const endingSlashRE = /\/$/
7
- export const outboundRE = /^[a-z]+:/i
3
+ export const isServer = Vue.prototype.$isServer;
4
+ export const hashRE = /#.*$/;
5
+ export const extRE = /\.(md|html)$/;
6
+ export const endingSlashRE = /\/$/;
7
+ export const outboundRE = /^[a-z]+:/i;
8
8
 
9
9
  export function normalize(path) {
10
- return decodeURI(path)
11
- .replace(hashRE, '')
12
- .replace(extRE, '')
10
+ return decodeURI(path).replace(hashRE, '').replace(extRE, '');
13
11
  }
14
12
 
15
13
  export function getHash(path) {
16
- const match = path.match(hashRE)
17
- if (match) {
18
- return match[0]
19
- }
14
+ const match = path.match(hashRE);
15
+ if (match) {
16
+ return match[0];
17
+ }
20
18
  }
21
19
 
22
20
  export function isExternal(path) {
23
- return outboundRE.test(path)
21
+ return outboundRE.test(path);
24
22
  }
25
23
 
26
24
  export function isMailto(path) {
27
- return /^mailto:/.test(path)
25
+ return /^mailto:/.test(path);
28
26
  }
29
27
 
30
28
  export function isTel(path) {
31
- return /^tel:/.test(path)
29
+ return /^tel:/.test(path);
32
30
  }
33
31
 
34
32
  export function ensureExt(path) {
35
- if (isExternal(path)) {
36
- return path
37
- }
38
- const hashMatch = path.match(hashRE)
39
- const hash = hashMatch ? hashMatch[0] : ''
40
- const normalized = normalize(path)
41
-
42
- if (endingSlashRE.test(normalized)) {
43
- return path
44
- }
45
- return normalized + '.html' + hash
33
+ if (isExternal(path)) {
34
+ return path;
35
+ }
36
+ const hashMatch = path.match(hashRE);
37
+ const hash = hashMatch ? hashMatch[0] : '';
38
+ const normalized = normalize(path);
39
+
40
+ if (endingSlashRE.test(normalized)) {
41
+ return path;
42
+ }
43
+ return normalized + '.html' + hash;
46
44
  }
47
45
 
48
46
  export function isActive(route, path) {
49
- const routeHash = decodeURIComponent(route.hash)
50
- const linkHash = getHash(path)
51
- if (linkHash && routeHash !== linkHash) {
52
- return false
53
- }
54
- const routePath = normalize(route.path)
55
- const pagePath = normalize(path)
56
- return routePath === pagePath
47
+ const routeHash = decodeURIComponent(route.hash);
48
+ const linkHash = getHash(path);
49
+ if (linkHash && routeHash !== linkHash) {
50
+ return false;
51
+ }
52
+ const routePath = normalize(route.path);
53
+ const pagePath = normalize(path);
54
+ return routePath === pagePath;
57
55
  }
58
56
 
59
57
  export function resolvePage(pages, rawPath, base) {
60
- if (isExternal(rawPath)) {
61
- return {
62
- type: 'external',
63
- path: rawPath
64
- }
65
- }
66
- if (base) {
67
- rawPath = resolvePath(rawPath, base)
68
- }
69
- const path = normalize(rawPath)
70
- const hash = rawPath.split('#')[1]
71
- for (let i = 0; i < pages.length; i++) {
72
- if (normalize(pages[i].regularPath) === path) {
73
- return Object.assign({}, pages[i], {
74
- type: 'page',
75
- path: ensureExt(pages[i].path) + (hash ? `#${hash}` : '')
76
- })
77
- }
78
- }
79
- console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`)
80
- return {}
58
+ if (isExternal(rawPath)) {
59
+ return {
60
+ type: 'external',
61
+ path: rawPath,
62
+ };
63
+ }
64
+ if (base) {
65
+ rawPath = resolvePath(rawPath, base);
66
+ }
67
+ const path = normalize(rawPath);
68
+ const hash = rawPath.split('#')[1];
69
+ for (let i = 0; i < pages.length; i++) {
70
+ if (normalize(pages[i].regularPath) === path) {
71
+ return Object.assign({}, pages[i], {
72
+ type: 'page',
73
+ path: ensureExt(pages[i].path) + (hash ? `#${hash}` : ''),
74
+ });
75
+ }
76
+ }
77
+ console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`);
78
+ return {};
81
79
  }
82
80
 
83
81
  function resolvePath(relative, base, append) {
84
- const firstChar = relative.charAt(0)
85
- if (firstChar === '/') {
86
- return relative
87
- }
88
-
89
- if (firstChar === '?' || firstChar === '#') {
90
- return base + relative
91
- }
92
-
93
- const stack = base.split('/')
94
-
95
- // remove trailing segment if:
96
- // - not appending
97
- // - appending to trailing slash (last segment is empty)
98
- if (!append || !stack[stack.length - 1]) {
99
- stack.pop()
100
- }
101
-
102
- // resolve relative path
103
- const segments = relative.replace(/^\//, '').split('/')
104
- for (let i = 0; i < segments.length; i++) {
105
- const segment = segments[i]
106
- if (segment === '..') {
107
- stack.pop()
108
- } else if (segment !== '.') {
109
- stack.push(segment)
110
- }
111
- }
112
-
113
- // ensure leading slash
114
- if (stack[0] !== '') {
115
- stack.unshift('')
116
- }
117
-
118
- return stack.join('/')
82
+ const firstChar = relative.charAt(0);
83
+ if (firstChar === '/') {
84
+ return relative;
85
+ }
86
+
87
+ if (firstChar === '?' || firstChar === '#') {
88
+ return base + relative;
89
+ }
90
+
91
+ const stack = base.split('/');
92
+
93
+ // remove trailing segment if:
94
+ // - not appending
95
+ // - appending to trailing slash (last segment is empty)
96
+ if (!append || !stack[stack.length - 1]) {
97
+ stack.pop();
98
+ }
99
+
100
+ // resolve relative path
101
+ const segments = relative.replace(/^\//, '').split('/');
102
+ for (let i = 0; i < segments.length; i++) {
103
+ const segment = segments[i];
104
+ if (segment === '..') {
105
+ stack.pop();
106
+ } else if (segment !== '.') {
107
+ stack.push(segment);
108
+ }
109
+ }
110
+
111
+ // ensure leading slash
112
+ if (stack[0] !== '') {
113
+ stack.unshift('');
114
+ }
115
+
116
+ return stack.join('/');
119
117
  }
120
118
 
121
119
  /**
@@ -126,29 +124,25 @@ function resolvePath(relative, base, append) {
126
124
  * @returns { SidebarGroup }
127
125
  */
128
126
  export function resolveSidebarItems(page, regularPath, site, localePath) {
129
- const { pages, themeConfig } = site
130
-
131
- const localeConfig = localePath && themeConfig.locales
132
- ? themeConfig.locales[localePath] || themeConfig
133
- : themeConfig
134
-
135
- const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar
136
- if (pageSidebarConfig === 'auto') {
137
- return resolveHeaders(page)
138
- }
139
-
140
- const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar
141
- if (!sidebarConfig) {
142
- return []
143
- } else {
144
- const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig)
145
- if (config === 'auto') {
146
- return resolveHeaders(page)
147
- }
148
- return config
149
- ? config.map(item => resolveItem(item, pages, base))
150
- : []
151
- }
127
+ const { pages, themeConfig } = site;
128
+
129
+ const localeConfig = localePath && themeConfig.locales ? themeConfig.locales[localePath] || themeConfig : themeConfig;
130
+
131
+ const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar;
132
+ if (pageSidebarConfig === 'auto') {
133
+ return resolveHeaders(page);
134
+ }
135
+
136
+ const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar;
137
+ if (!sidebarConfig) {
138
+ return [];
139
+ } else {
140
+ const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig);
141
+ if (config === 'auto') {
142
+ return resolveHeaders(page);
143
+ }
144
+ return config ? config.map(item => resolveItem(item, pages, base)) : [];
145
+ }
152
146
  }
153
147
 
154
148
  /**
@@ -156,41 +150,43 @@ export function resolveSidebarItems(page, regularPath, site, localePath) {
156
150
  * @returns { SidebarGroup }
157
151
  */
158
152
  function resolveHeaders(page) {
159
- const headers = groupHeaders(page.headers || [])
160
- return [{
161
- type: 'group',
162
- collapsable: false,
163
- title: page.title,
164
- path: null,
165
- children: headers.map(h => ({
166
- type: 'auto',
167
- title: h.title,
168
- basePath: page.path,
169
- path: page.path + '#' + h.slug,
170
- children: h.children || []
171
- }))
172
- }]
153
+ const headers = groupHeaders(page.headers || []);
154
+ return [
155
+ {
156
+ type: 'group',
157
+ collapsable: false,
158
+ title: page.title,
159
+ path: null,
160
+ children: headers.map(h => ({
161
+ type: 'auto',
162
+ title: h.title,
163
+ basePath: page.path,
164
+ path: page.path + '#' + h.slug,
165
+ children: h.children || [],
166
+ })),
167
+ },
168
+ ];
173
169
  }
174
170
 
175
171
  export function groupHeaders(headers) {
176
- // group h3s under h2
177
- headers = headers.map(h => Object.assign({}, h))
178
- let lastH2
179
- headers.forEach(h => {
180
- if (h.level === 2) {
181
- lastH2 = h
182
- } else if (lastH2) {
183
- (lastH2.children || (lastH2.children = [])).push(h)
184
- }
185
- })
186
- // TODO h1-h3
187
- return headers.filter(h => h.level === 2)
172
+ // group h3s under h2
173
+ headers = headers.map(h => Object.assign({}, h));
174
+ let lastH2;
175
+ headers.forEach(h => {
176
+ if (h.level === 2) {
177
+ lastH2 = h;
178
+ } else if (lastH2) {
179
+ (lastH2.children || (lastH2.children = [])).push(h);
180
+ }
181
+ });
182
+ // TODO h1-h3
183
+ return headers.filter(h => h.level === 2);
188
184
  }
189
185
 
190
186
  export function resolveNavLinkItem(linkItem) {
191
- return Object.assign(linkItem, {
192
- type: linkItem.items && linkItem.items.length ? 'links' : 'link'
193
- })
187
+ return Object.assign(linkItem, {
188
+ type: linkItem.items && linkItem.items.length ? 'links' : 'link',
189
+ });
194
190
  }
195
191
 
196
192
  /**
@@ -199,100 +195,98 @@ export function resolveNavLinkItem(linkItem) {
199
195
  * @returns { base: string, config: SidebarConfig }
200
196
  */
201
197
  export function resolveMatchingConfig(regularPath, config) {
202
- if (Array.isArray(config)) {
203
- return {
204
- base: '/',
205
- config: config
206
- }
207
- }
208
- for (const base in config) {
209
- if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) {
210
- return {
211
- base,
212
- config: config[base]
213
- }
214
- }
215
- }
216
- return {}
198
+ if (Array.isArray(config)) {
199
+ return {
200
+ base: '/',
201
+ config: config,
202
+ };
203
+ }
204
+ for (const base in config) {
205
+ if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) {
206
+ return {
207
+ base,
208
+ config: config[base],
209
+ };
210
+ }
211
+ }
212
+ return {};
217
213
  }
218
214
 
219
215
  function ensureEndingSlash(path) {
220
- return /(\.html|\/)$/.test(path)
221
- ? path
222
- : path + '/'
216
+ return /(\.html|\/)$/.test(path) ? path : path + '/';
223
217
  }
224
218
 
225
219
  function resolveItem(item, pages, base, groupDepth = 1) {
226
- if (typeof item === 'string') {
227
- return resolvePage(pages, item, base)
228
- } else if (Array.isArray(item)) {
229
- return Object.assign(resolvePage(pages, item[0], base), {
230
- title: item[1]
231
- })
232
- } else {
233
- const children = item.children || []
234
- if (children.length === 0 && item.path) {
235
- return Object.assign(resolvePage(pages, item.path, base), {
236
- title: item.title
237
- })
238
- }
239
- return {
240
- type: 'group',
241
- path: item.path,
242
- title: item.title,
243
- sidebarDepth: item.sidebarDepth,
244
- initialOpenGroupIndex: item.initialOpenGroupIndex,
245
- children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)),
246
- collapsable: item.collapsable !== false
247
- }
248
- }
220
+ if (typeof item === 'string') {
221
+ return resolvePage(pages, item, base);
222
+ } else if (Array.isArray(item)) {
223
+ return Object.assign(resolvePage(pages, item[0], base), {
224
+ title: item[1],
225
+ });
226
+ } else {
227
+ const children = item.children || [];
228
+ if (children.length === 0 && item.path) {
229
+ return Object.assign(resolvePage(pages, item.path, base), {
230
+ title: item.title,
231
+ });
232
+ }
233
+ return {
234
+ type: 'group',
235
+ path: item.path,
236
+ title: item.title,
237
+ sidebarDepth: item.sidebarDepth,
238
+ initialOpenGroupIndex: item.initialOpenGroupIndex,
239
+ children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)),
240
+ collapsable: item.collapsable !== false,
241
+ };
242
+ }
249
243
  }
250
244
 
251
245
  export function forbidScroll(use = true) {
252
- if (use) {
253
- const classList = document.body.className.split(' ')
254
- classList.push('forbid_scroll')
255
- document.body.className = classList.join(' ')
256
- } else {
257
- document.body.className = document.body.className.replace(/\s+forbid_scroll/g, '')
258
- }
246
+ if (use) {
247
+ const classList = document.body.className.split(' ');
248
+ classList.push('forbid_scroll');
249
+ document.body.className = classList.join(' ');
250
+ } else {
251
+ document.body.className = document.body.className.replace(/\s+forbid_scroll/g, '');
252
+ }
259
253
  }
260
254
 
261
255
  export const os = {
262
- android: false,
263
- ios: false,
264
- mobile: false,
265
- pc: false,
266
- init: function () {
267
- var ua = navigator.userAgent;
268
- if (ua.match(/(Android);?[\s\/]+([\d+.]+)?/)) {
269
- this.android = true;
270
- this.mobile = true;
271
- this.pc = false;
272
- } else if (ua.match(/(iPhone\sOS)\s([\d_]+)/)) {
273
- this.ios = true;
274
- this.mobile = true;
275
- this.pc = false;
276
- } else {
277
- this.android = false;
278
- this.ios = false;
279
- this.mobile = false;
280
- this.pc = true;
281
- }
282
- }
256
+ android: false,
257
+ ios: false,
258
+ mobile: false,
259
+ pc: false,
260
+ init: function () {
261
+ var ua = navigator.userAgent;
262
+ if (ua.match(/(Android);?[\s\/]+([\d+.]+)?/)) {
263
+ this.android = true;
264
+ this.mobile = true;
265
+ this.pc = false;
266
+ } else if (ua.match(/(iPhone\sOS)\s([\d_]+)/)) {
267
+ this.ios = true;
268
+ this.mobile = true;
269
+ this.pc = false;
270
+ } else {
271
+ this.android = false;
272
+ this.ios = false;
273
+ this.mobile = false;
274
+ this.pc = true;
275
+ }
276
+ },
283
277
  };
284
278
 
285
279
  export function debounce(fn, delay) {
286
- let timeout
287
- const newFn = function () {
288
- clearTimeout(timeout)
289
- const timerFn = () => fn.apply(this, arguments)
290
- timeout = setTimeout(timerFn, delay)
291
- }
292
- newFn.cancel = function () {
293
- clearTimeout(timeout)
294
- }
295
- return newFn
280
+ let timeout;
281
+ const newFn = function () {
282
+ clearTimeout(timeout);
283
+ const timerFn = () => fn.apply(this, arguments);
284
+ timeout = setTimeout(timerFn, delay);
285
+ };
286
+ newFn.cancel = function () {
287
+ clearTimeout(timeout);
288
+ };
289
+ return newFn;
296
290
  }
297
291
 
298
292
  /*
@@ -303,31 +297,105 @@ export function debounce(fn, delay) {
303
297
  * @returns {Element}
304
298
  */
305
299
  export function findContainerInVm(ref, vm, def) {
306
- if (!ref) return def
307
- let container
308
- let parent = vm
309
- while ((parent = parent.$parent) && !container) {
310
- container = parent.$refs[ref]
311
- }
312
- // Ensure it's html element (ref could be component)
313
- if (container && container.$el) {
314
- container = container.$el
315
- }
316
- return container || def
300
+ if (!ref) return def;
301
+ let container;
302
+ let parent = vm;
303
+ while ((parent = parent.$parent) && !container) {
304
+ container = parent.$refs[ref];
305
+ }
306
+ // Ensure it's html element (ref could be component)
307
+ if (container && container.$el) {
308
+ container = container.$el;
309
+ }
310
+ return container || def;
317
311
  }
318
312
 
319
313
  export function once(fn, ctx = null) {
320
- let res
321
- return (...args) => {
322
- if (fn) {
323
- res = fn.apply(ctx, args)
324
- fn = null
325
- }
326
- return res
327
- }
314
+ let res;
315
+ return (...args) => {
316
+ if (fn) {
317
+ res = fn.apply(ctx, args);
318
+ fn = null;
319
+ }
320
+ return res;
321
+ };
328
322
  }
329
323
 
330
324
  export const getNavbarHeight = () => {
331
- const { height, top } = document.querySelector('.navbar').getBoundingClientRect()
332
- return height + top
333
- }
325
+ const { height, top } = document.querySelector('.navbar').getBoundingClientRect();
326
+ return height + top;
327
+ };
328
+
329
+ export const usePopoverDirective = selector => {
330
+ let mouseOptions = {};
331
+ const mouseEnter = e => {
332
+ const { top, left, bottom, right, height, width } = e.target.getBoundingClientRect();
333
+ mouseOptions = {
334
+ top,
335
+ left,
336
+ bottom,
337
+ right,
338
+ height,
339
+ width,
340
+ };
341
+ };
342
+
343
+ return {
344
+ mouseEnter,
345
+ directive: {
346
+ inserted(el, binding, vnode, prevVnode) {
347
+ const insertContent = document.querySelector(selector);
348
+ if (insertContent) {
349
+ insertContent.appendChild(el);
350
+ }
351
+ },
352
+ componentUpdated(el, binding, vnode, prevVnode) {
353
+ const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
354
+ const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
355
+ const screenWidth = document.documentElement.clientWidth;
356
+
357
+ const { width, height } = el.getBoundingClientRect();
358
+ const {
359
+ left: targetLeft,
360
+ top: targetTop,
361
+ bottom: targetBottom,
362
+ right: targetRight,
363
+ height: targetHeight,
364
+ width: targetWidth,
365
+ } = mouseOptions;
366
+ const OFFSET = 10;
367
+ const NAVBAR_HEIGHT = getNavbarHeight();
368
+
369
+ // 默认 icon 正中间位置
370
+ const HALF_WIDTH = width / 2;
371
+ const HALF_TARGET_WIDTH = targetWidth / 2;
372
+ const TOP = height; // + OFFSET
373
+ const LEFT = HALF_TARGET_WIDTH - HALF_WIDTH;
374
+
375
+ const DEFAULT_TOP = targetTop + scrollTop;
376
+ const DEFAULT_LEFT = targetLeft + scrollLeft;
377
+
378
+ let currentTop = DEFAULT_TOP - TOP;
379
+ let currentLeft = DEFAULT_LEFT + LEFT;
380
+
381
+ if (targetTop - NAVBAR_HEIGHT - TOP - OFFSET > 0) {
382
+ // 上方空间够
383
+ currentTop = currentTop - OFFSET;
384
+ } else {
385
+ // 上方空间不够
386
+ currentTop = currentTop + (height + targetHeight) + OFFSET;
387
+ }
388
+
389
+ const RIGHT_OFFSET = screenWidth - (targetLeft + HALF_TARGET_WIDTH) - HALF_WIDTH;
390
+ // const LEFT_OFFSET = (screenWidth - targetLeft - HALF_TARGET_WIDTH) - HALF_WIDTH
391
+
392
+ if (RIGHT_OFFSET < 0) {
393
+ // 右侧空间不够
394
+ currentLeft = screenWidth - width;
395
+ }
396
+ el.style.top = `${currentTop < 0 ? 0 : currentTop}px`;
397
+ el.style.left = `${currentLeft < 0 ? 0 : currentLeft}px`;
398
+ },
399
+ },
400
+ };
401
+ };