@opentiny/vue-docs 3.23.0 → 3.24.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.
- package/demos/apis/dialog-select.js +42 -0
- package/demos/apis/popeditor.js +14 -0
- package/demos/apis/steps.js +15 -0
- package/demos/apis/text-popup.js +2 -2
- package/demos/apis/user-head.js +7 -18
- package/demos/mobile-first/app/pager/webdoc/pager.js +0 -48
- package/demos/mobile-first/app/steps/vertical.vue +14 -1
- package/demos/pc/app/calendar-view/calendar-event.spec.ts +2 -2
- package/demos/pc/app/date-panel/basic-usage.spec.ts +6 -6
- package/demos/pc/app/date-panel/custom-week.spec.ts +1 -1
- package/demos/pc/app/date-panel/disabled-date.spec.ts +8 -9
- package/demos/pc/app/date-panel/event.spec.ts +4 -4
- package/demos/pc/app/date-panel/format.spec.ts +2 -2
- package/demos/pc/app/date-panel/readonly.spec.ts +3 -3
- package/demos/pc/app/date-panel/unlink-panels.spec.ts +4 -4
- package/demos/pc/app/date-picker/basic-usage.spec.ts +2 -2
- package/demos/pc/app/date-picker/date-range.spec.ts +3 -3
- package/demos/pc/app/dialog-select/nest-grid-multi-composition-api.vue +19 -1
- package/demos/pc/app/dialog-select/nest-grid-multi.spec.ts +19 -0
- package/demos/pc/app/dialog-select/nest-grid-multi.vue +17 -1
- package/demos/pc/app/dialog-select/webdoc/dialog-select.js +2 -2
- package/demos/pc/app/grid/base/basic-usage-composition-api.vue +48 -93
- package/demos/pc/app/grid/base/basic-usage.spec.js +1 -1
- package/demos/pc/app/grid/base/basic-usage.vue +29 -132
- package/demos/pc/app/grid/dynamically-columns/dynamically-columns.spec.js +3 -3
- package/demos/pc/app/grid/webdoc/grid-ai-agent.js +23 -0
- package/demos/pc/app/popeditor/condition-layout-composition-api.vue +1 -0
- package/demos/pc/app/popeditor/condition-layout.spec.ts +1 -0
- package/demos/pc/app/popeditor/condition-layout.vue +1 -0
- package/demos/pc/app/popeditor/webdoc/popeditor.js +2 -2
- package/demos/pc/app/qr-code/style-composition-api.vue +14 -3
- package/demos/pc/app/qr-code/style.vue +15 -3
- package/demos/pc/app/text-popup/{value.spec.ts → modelValue.spec.ts} +2 -2
- package/demos/pc/app/text-popup/webdoc/text-popup.js +8 -8
- package/demos/pc/webdoc/changelog.md +461 -451
- package/package.json +27 -19
- package/playground/App.vue +2 -2
- package/src/App.vue +18 -1
- package/src/{views/components-doc/components → components}/demo.vue +1 -1
- package/src/{views/components-doc/components → components}/float-settings.vue +24 -7
- package/src/components/mcp-docs.vue +55 -0
- package/src/components/tiny-robot-chat.vue +128 -0
- package/src/composable/DifyModelProvider.ts +65 -0
- package/src/composable/storage.ts +71 -0
- package/src/composable/useTinyRobot.ts +167 -0
- package/src/composable/utils.ts +172 -0
- package/src/i18n/index.js +5 -2
- package/src/main.js +10 -1
- package/src/router.js +9 -0
- package/src/tools/appData.js +12 -2
- package/src/views/components-doc/common.vue +22 -8
- package/src/views/comprehensive/Demo.vue +211 -0
- package/src/views/comprehensive/index.vue +391 -0
- package/src/views/comprehensive/products.json +99 -0
- package/src/views/comprehensive/types/index.ts +37 -0
- package/src/views/layout/layout.vue +2 -2
- package/demos/mobile-first/app/pager/current-change.vue +0 -34
- package/demos/mobile-first/app/pager/next-click.vue +0 -34
- package/demos/mobile-first/app/pager/prev-click.vue +0 -34
- /package/demos/pc/app/text-popup/{clear-value-composition-api.vue → clear-modelValue-composition-api.vue} +0 -0
- /package/demos/pc/app/text-popup/{clear-value.spec.ts → clear-modelValue.spec.ts} +0 -0
- /package/demos/pc/app/text-popup/{clear-value.vue → clear-modelValue.vue} +0 -0
- /package/demos/pc/app/text-popup/{value-composition-api.vue → modelValue-composition-api.vue} +0 -0
- /package/demos/pc/app/text-popup/{value.vue → modelValue.vue} +0 -0
- /package/src/{views/components-doc/components → components}/anchor.vue +0 -0
- /package/src/{views/components-doc/components → components}/api-docs.vue +0 -0
- /package/src/{views/components-doc/components → components}/async-highlight.vue +0 -0
- /package/src/{views/components-doc/components → components}/contributor.vue +0 -0
- /package/src/{views/components-doc/components → components}/header.vue +0 -0
- /package/src/{views/components-doc/components → components}/version-tip.vue +0 -0
- /package/src/{views/components-doc/composition → composable}/useTasksFinish.ts +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具函数模块
|
|
3
|
+
* 提供一些实用的辅助函数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ChatMessage, ChatCompletionResponse, StreamHandler } from '@opentiny/tiny-robot-kit'
|
|
7
|
+
import type { ChatCompletionRequest } from '@opentiny/tiny-robot-kit'
|
|
8
|
+
import { ref, type Ref } from 'vue'
|
|
9
|
+
|
|
10
|
+
export { $local } from './storage'
|
|
11
|
+
|
|
12
|
+
export const showTinyRobot = ref(true)
|
|
13
|
+
|
|
14
|
+
export const globalConversation = {
|
|
15
|
+
id: '',
|
|
16
|
+
sessionId: ''
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 处理SSE流式响应
|
|
20
|
+
* @param response fetch响应对象
|
|
21
|
+
* @param handler 流处理器
|
|
22
|
+
*/
|
|
23
|
+
export async function handleSSEStream(
|
|
24
|
+
response: Response,
|
|
25
|
+
handler: StreamHandler,
|
|
26
|
+
message: Ref<ChatCompletionRequest['messages']>,
|
|
27
|
+
signal?: AbortSignal
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
// 获取ReadableStream
|
|
30
|
+
const reader = response.body?.getReader()
|
|
31
|
+
if (!reader) {
|
|
32
|
+
throw new Error('Response body is null')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 处理流式数据
|
|
36
|
+
const decoder = new TextDecoder()
|
|
37
|
+
let buffer = ''
|
|
38
|
+
|
|
39
|
+
if (signal) {
|
|
40
|
+
signal.addEventListener(
|
|
41
|
+
'abort',
|
|
42
|
+
() => {
|
|
43
|
+
reader.cancel().catch((err) => console.error('Error cancelling reader:', err))
|
|
44
|
+
},
|
|
45
|
+
{ once: true }
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let messageIndex = 0
|
|
50
|
+
function printMessage(data, str: string, endln = false) {
|
|
51
|
+
handler.onData({
|
|
52
|
+
id: '',
|
|
53
|
+
created: data.created_at,
|
|
54
|
+
choices: [
|
|
55
|
+
{
|
|
56
|
+
index: messageIndex++,
|
|
57
|
+
delta: {
|
|
58
|
+
role: 'assistant',
|
|
59
|
+
content: str + (endln ? '\n\n' : '')
|
|
60
|
+
},
|
|
61
|
+
finish_reason: null
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
object: '',
|
|
65
|
+
model: ''
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
while (true) {
|
|
70
|
+
if (signal?.aborted) {
|
|
71
|
+
await reader.cancel()
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { done, value } = await reader.read()
|
|
76
|
+
if (done) break
|
|
77
|
+
|
|
78
|
+
// 解码二进制数据
|
|
79
|
+
const chunk = decoder.decode(value, { stream: true })
|
|
80
|
+
buffer += chunk
|
|
81
|
+
|
|
82
|
+
// 处理完整的SSE消息
|
|
83
|
+
const lines = buffer.split('\n\n')
|
|
84
|
+
buffer = lines.pop() || ''
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (line.trim() === '') continue
|
|
88
|
+
if (line.trim() === 'data: [DONE]') {
|
|
89
|
+
handler.onDone()
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// 解析SSE消息
|
|
95
|
+
const dataMatch = line.match(/^data: (.+)$/m)
|
|
96
|
+
if (!dataMatch) continue
|
|
97
|
+
|
|
98
|
+
const data = JSON.parse(dataMatch[1])
|
|
99
|
+
// console.log('SSE data:', data)
|
|
100
|
+
if (data?.event === 'node_started') {
|
|
101
|
+
printMessage(data, `${data.data.title} 节点运行...`, true)
|
|
102
|
+
}
|
|
103
|
+
if (data?.event === 'node_finished') {
|
|
104
|
+
printMessage(
|
|
105
|
+
data,
|
|
106
|
+
`${data.data.title} 节点结束\n\n` +
|
|
107
|
+
(data.data.node_type === 'answer' ? `${data.data.outputs.answer}` : '')
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
if (data?.event === 'agent_log' && data.data.status === 'success' && data.data.label.startsWith('CALL')) {
|
|
111
|
+
printMessage(data, `--${data.data.label}(${JSON.stringify(data.data.data.output.tool_call_input)})`, true)
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('Error parsing SSE message:', error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (buffer.trim() === 'data: [DONE]' || signal?.aborted) {
|
|
120
|
+
handler.onDone()
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (signal?.aborted) return
|
|
124
|
+
throw error
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 格式化消息
|
|
130
|
+
* 将各种格式的消息转换为标准的ChatMessage格式
|
|
131
|
+
* @param messages 消息数组
|
|
132
|
+
* @returns 标准格式的消息数组
|
|
133
|
+
*/
|
|
134
|
+
export function formatMessages(messages: Array<ChatMessage | string>): ChatMessage[] {
|
|
135
|
+
return messages.map((msg) => {
|
|
136
|
+
// 如果已经是标准格式,直接返回
|
|
137
|
+
if (typeof msg === 'object' && 'role' in msg && 'content' in msg) {
|
|
138
|
+
return {
|
|
139
|
+
role: msg.role,
|
|
140
|
+
content: String(msg.content),
|
|
141
|
+
...(msg.name ? { name: msg.name } : {})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 如果是字符串,默认为用户消息
|
|
146
|
+
if (typeof msg === 'string') {
|
|
147
|
+
return {
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: msg
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 其他情况,尝试转换为字符串
|
|
154
|
+
return {
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: String(msg)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 从响应中提取文本内容
|
|
163
|
+
* @param response 聊天完成响应
|
|
164
|
+
* @returns 文本内容
|
|
165
|
+
*/
|
|
166
|
+
export function extractTextFromResponse(response: ChatCompletionResponse): string {
|
|
167
|
+
if (!response.choices || !response.choices.length) {
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return response.choices[0].message?.content || ''
|
|
172
|
+
}
|
package/src/i18n/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createI18n } from 'vue-i18n'
|
|
2
|
-
import { initI18n } from '@opentiny/vue-locale'
|
|
2
|
+
import { initI18n, t } from '@opentiny/vue-locale'
|
|
3
3
|
import { $local } from '../tools'
|
|
4
4
|
import zh from './zh.json'
|
|
5
5
|
import en from './en.json'
|
|
6
|
+
import { zhCN, enUS } from '@opentiny/tiny-vue-mcp'
|
|
6
7
|
|
|
7
|
-
const messages = { enUS: en, zhCN: zh }
|
|
8
|
+
const messages = { enUS: { ...en, ...enUS }, zhCN: { ...zh, ...zhCN } }
|
|
8
9
|
// $local._lang = $local._lang !== 'zhCN' && $local._lang !== 'enUS' ? 'zhCN' : $local._lang
|
|
9
10
|
$local._lang = 'zhCN'
|
|
10
11
|
const customCreateI18n = ({ locale, messages }) =>
|
|
@@ -26,3 +27,5 @@ const i18nByKey = i18n.global.t
|
|
|
26
27
|
const getWord = (cn, en) => (i18n.global.locale === 'zhCN' ? cn : en)
|
|
27
28
|
|
|
28
29
|
export { i18n, i18nByKey, getWord }
|
|
30
|
+
|
|
31
|
+
export { t }
|
package/src/main.js
CHANGED
|
@@ -2,6 +2,9 @@ import { createHead } from '@vueuse/head'
|
|
|
2
2
|
import { createApp } from 'vue'
|
|
3
3
|
import '@unocss/reset/eric-meyer.css'
|
|
4
4
|
|
|
5
|
+
// tiny-robot 对话框
|
|
6
|
+
import '@opentiny/tiny-robot/dist/style.css'
|
|
7
|
+
|
|
5
8
|
// markdown文件内代码高亮
|
|
6
9
|
import 'prismjs/themes/prism.css'
|
|
7
10
|
import 'uno.css'
|
|
@@ -16,7 +19,7 @@ import './assets/custom-markdown.css'
|
|
|
16
19
|
import './assets/custom-block.less'
|
|
17
20
|
import './assets/md-preview.less'
|
|
18
21
|
|
|
19
|
-
import { i18n } from './i18n/index'
|
|
22
|
+
import { i18n, t } from './i18n/index'
|
|
20
23
|
import { router } from './router'
|
|
21
24
|
import App from './App.vue'
|
|
22
25
|
import { appData } from './tools'
|
|
@@ -33,6 +36,12 @@ import '@docsearch/css'
|
|
|
33
36
|
import { doSearchEverySite } from './tools/docsearch'
|
|
34
37
|
import '@opentiny/vue-theme/dark-theme-index.css'
|
|
35
38
|
|
|
39
|
+
import { registerMcpConfig } from '@opentiny/vue-common'
|
|
40
|
+
import { createMcpTools, getTinyVueMcpConfig } from '@opentiny/tiny-vue-mcp'
|
|
41
|
+
|
|
42
|
+
// 注册TinyVue组件mcp配置
|
|
43
|
+
registerMcpConfig(getTinyVueMcpConfig({ t }), createMcpTools)
|
|
44
|
+
|
|
36
45
|
const envTarget = import.meta.env.VITE_BUILD_TARGET || 'open'
|
|
37
46
|
|
|
38
47
|
hljs.registerLanguage('javascript', javascript)
|
package/src/router.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
2
|
import Layout from '@/views/layout/layout.vue'
|
|
3
3
|
import { LANG_PATH_MAP, ZH_CN_LANG, DEFAULT_THEME } from './const'
|
|
4
|
+
import { appData } from './tools/appData.js'
|
|
4
5
|
|
|
5
6
|
const Components = () => import('@/views/components-doc/index.vue')
|
|
6
7
|
const Docs = () => import('@/views/docs/docs.vue')
|
|
7
8
|
const Overview = () => import('@/views/overview.vue')
|
|
8
9
|
const Features = () => import('@/views/features.vue')
|
|
10
|
+
const Comprehensive = () => import('@/views/comprehensive/index.vue')
|
|
9
11
|
|
|
10
12
|
const context = import.meta.env.VITE_CONTEXT
|
|
11
13
|
|
|
@@ -17,6 +19,11 @@ let routes = [
|
|
|
17
19
|
name: 'overview',
|
|
18
20
|
children: [{ name: 'Overview', path: '', component: Overview, meta: { title: '组件总览 | TinyVue' } }]
|
|
19
21
|
},
|
|
22
|
+
{
|
|
23
|
+
path: `${context}:all?/zh-CN/:theme/comprehensive`,
|
|
24
|
+
component: Comprehensive,
|
|
25
|
+
name: 'comprehensive'
|
|
26
|
+
},
|
|
20
27
|
// 文档
|
|
21
28
|
{
|
|
22
29
|
path: `${context}:all?/:lang/:theme/docs/:docId`,
|
|
@@ -57,5 +64,7 @@ router.afterEach((to, from) => {
|
|
|
57
64
|
if (to.meta.title) {
|
|
58
65
|
document.title = to.meta.title
|
|
59
66
|
}
|
|
67
|
+
// tiny-robot 通过路由,确定浮动区,是否显示AI按钮
|
|
68
|
+
appData.hasFloatRobot = to.path.endsWith('components/grid')
|
|
60
69
|
})
|
|
61
70
|
export { router }
|
package/src/tools/appData.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { reactive, computed } from 'vue'
|
|
1
|
+
import { reactive, computed, watch } from 'vue'
|
|
2
2
|
import { useAutoStore } from './storage'
|
|
3
3
|
import { useMediaQuery } from './useMediaQuery'
|
|
4
4
|
import { ZH_CN_LANG, EN_US_LANG, LANG_KEY, LANG_PATH_MAP } from '../const'
|
|
@@ -8,7 +8,9 @@ const enPath = LANG_PATH_MAP[EN_US_LANG]
|
|
|
8
8
|
const appData = reactive({
|
|
9
9
|
lang: useAutoStore('local', LANG_KEY, ZH_CN_LANG),
|
|
10
10
|
theme: useAutoStore('local', '_theme', 'light'),
|
|
11
|
-
bpState: useMediaQuery([640, 1024, 1280]).matches // 3点4区间, bp0,bp1,bp2,bp3
|
|
11
|
+
bpState: useMediaQuery([640, 1024, 1280]).matches, // 3点4区间, bp0,bp1,bp2,bp3
|
|
12
|
+
showTinyRobot: false,
|
|
13
|
+
hasFloatRobot: false
|
|
12
14
|
})
|
|
13
15
|
const isZhCn = computed(() => appData.lang === ZH_CN_LANG)
|
|
14
16
|
const appFn = {
|
|
@@ -27,4 +29,12 @@ const appFn = {
|
|
|
27
29
|
}
|
|
28
30
|
// 为了和tiny-vue共享同一个响应变量
|
|
29
31
|
window.appData = appData
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => appData.showTinyRobot,
|
|
35
|
+
(value) => {
|
|
36
|
+
document.body.classList.toggle('docs-on-robot-show', value)
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
30
40
|
export { appData, appFn, isZhCn }
|
|
@@ -62,7 +62,11 @@
|
|
|
62
62
|
@jump-to-demo="jumpToDemo"
|
|
63
63
|
></api-docs>
|
|
64
64
|
</tiny-tab-item>
|
|
65
|
+
<tiny-tab-item v-if="appData.hasFloatRobot" title="MCP" name="MCP">
|
|
66
|
+
<McpDocs :name="state.cmpId" />
|
|
67
|
+
</tiny-tab-item>
|
|
65
68
|
</tiny-tabs>
|
|
69
|
+
|
|
66
70
|
<slot name="main-right" />
|
|
67
71
|
</div>
|
|
68
72
|
|
|
@@ -84,6 +88,7 @@
|
|
|
84
88
|
</div>
|
|
85
89
|
<div id="footer"></div>
|
|
86
90
|
</div>
|
|
91
|
+
<robotChat v-if="appData.showTinyRobot && appData.hasFloatRobot"></robotChat>
|
|
87
92
|
</template>
|
|
88
93
|
|
|
89
94
|
<script setup lang="ts">
|
|
@@ -94,12 +99,16 @@ import { debounce } from '@opentiny/utils'
|
|
|
94
99
|
import { i18nByKey, getWord, $clone, useApiMode } from '@/tools'
|
|
95
100
|
import { router } from '@/router.js'
|
|
96
101
|
import { getWebdocPath } from './cmp-config'
|
|
97
|
-
import DemoBox from '
|
|
98
|
-
import AsideAnchor from '
|
|
99
|
-
import ComponentHeader from '
|
|
100
|
-
import ComponentContributor from '
|
|
101
|
-
import ApiDocs from '
|
|
102
|
-
import
|
|
102
|
+
import DemoBox from '../../components/demo.vue'
|
|
103
|
+
import AsideAnchor from '../../components/anchor.vue'
|
|
104
|
+
import ComponentHeader from '../../components/header.vue'
|
|
105
|
+
import ComponentContributor from '../../components/contributor.vue'
|
|
106
|
+
import ApiDocs from '../../components/api-docs.vue'
|
|
107
|
+
import McpDocs from '../../components/mcp-docs.vue'
|
|
108
|
+
import useTasksFinish from '../../composable/useTasksFinish'
|
|
109
|
+
import { appData } from '../../tools/appData'
|
|
110
|
+
|
|
111
|
+
import robotChat from '../../components/tiny-robot-chat.vue'
|
|
103
112
|
|
|
104
113
|
const props = defineProps({ loadData: {}, appMode: {}, demoKey: {} })
|
|
105
114
|
|
|
@@ -166,8 +175,10 @@ watch(
|
|
|
166
175
|
onMounted(() => {
|
|
167
176
|
loadPage()
|
|
168
177
|
// 加载公共尾部
|
|
169
|
-
|
|
170
|
-
|
|
178
|
+
nextTick(() => {
|
|
179
|
+
const common = new window.TDCommon(['#footer'], { allowDarkTheme: true })
|
|
180
|
+
common.renderFooter()
|
|
181
|
+
})
|
|
171
182
|
setScrollListener()
|
|
172
183
|
})
|
|
173
184
|
|
|
@@ -438,6 +449,9 @@ defineExpose({ loadPage })
|
|
|
438
449
|
</script>
|
|
439
450
|
|
|
440
451
|
<style lang="less" scoped>
|
|
452
|
+
:global(.docs-on-robot-show .docs-content) {
|
|
453
|
+
margin-right: 480px;
|
|
454
|
+
}
|
|
441
455
|
.docs-content {
|
|
442
456
|
flex: 1;
|
|
443
457
|
overflow: hidden auto;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="products-page">
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h3>商品管理</h3>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="page-content">
|
|
7
|
+
<div class="button-box">
|
|
8
|
+
<tiny-button type="info" @click="addProductToEdit"> 添加商品 </tiny-button>
|
|
9
|
+
<tiny-button type="danger" @click="removeProduct"> 删除商品 </tiny-button>
|
|
10
|
+
<tiny-button type="success" @click="saveProduct"> 保存 </tiny-button>
|
|
11
|
+
</div>
|
|
12
|
+
<tiny-grid
|
|
13
|
+
auto-resize
|
|
14
|
+
ref="gridRef"
|
|
15
|
+
:data="products"
|
|
16
|
+
:height="500"
|
|
17
|
+
:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }"
|
|
18
|
+
:tiny_mcp_config="{
|
|
19
|
+
server,
|
|
20
|
+
business: {
|
|
21
|
+
id: 'product-list',
|
|
22
|
+
description: '商品列表'
|
|
23
|
+
}
|
|
24
|
+
}"
|
|
25
|
+
>
|
|
26
|
+
<tiny-grid-column type="index" width="50" />
|
|
27
|
+
<tiny-grid-column type="selection" width="50" />
|
|
28
|
+
<tiny-grid-column title="商品图片" width="100">
|
|
29
|
+
<template #default="{ row }">
|
|
30
|
+
<tiny-image :src="row.image" :preview-src-list="[row.image]" class="product-image" />
|
|
31
|
+
</template>
|
|
32
|
+
</tiny-grid-column>
|
|
33
|
+
|
|
34
|
+
<tiny-grid-column field="name" title="商品名称" :editor="{ component: 'input' }" />
|
|
35
|
+
<tiny-grid-column
|
|
36
|
+
field="price"
|
|
37
|
+
:editor="{
|
|
38
|
+
component: 'input',
|
|
39
|
+
attrs: { type: 'number' }
|
|
40
|
+
}"
|
|
41
|
+
title="价格"
|
|
42
|
+
>
|
|
43
|
+
<template #default="{ row }"> ¥{{ row.price }} </template>
|
|
44
|
+
</tiny-grid-column>
|
|
45
|
+
<tiny-grid-column
|
|
46
|
+
field="stock"
|
|
47
|
+
:editor="{
|
|
48
|
+
component: 'input',
|
|
49
|
+
attrs: { type: 'number' }
|
|
50
|
+
}"
|
|
51
|
+
title="库存"
|
|
52
|
+
/>
|
|
53
|
+
<tiny-grid-column
|
|
54
|
+
field="category"
|
|
55
|
+
:editor="{
|
|
56
|
+
component: 'select',
|
|
57
|
+
options: [
|
|
58
|
+
{ label: '手机', value: 'phones' },
|
|
59
|
+
{ label: '笔记本', value: 'laptops' },
|
|
60
|
+
{ label: '平板', value: 'tablets' }
|
|
61
|
+
]
|
|
62
|
+
}"
|
|
63
|
+
title="分类"
|
|
64
|
+
>
|
|
65
|
+
<template #default="{ row }">
|
|
66
|
+
{{ categoryLabels[row.category] }}
|
|
67
|
+
</template>
|
|
68
|
+
</tiny-grid-column>
|
|
69
|
+
<tiny-grid-column
|
|
70
|
+
field="status"
|
|
71
|
+
:editor="{
|
|
72
|
+
component: 'select',
|
|
73
|
+
options: [
|
|
74
|
+
{ label: '上架', value: 'on' },
|
|
75
|
+
{ label: '下架', value: 'off' }
|
|
76
|
+
]
|
|
77
|
+
}"
|
|
78
|
+
title="状态"
|
|
79
|
+
>
|
|
80
|
+
<template #default="{ row }">
|
|
81
|
+
<tiny-tag :type="row.status === 'on' ? 'success' : 'warning'">
|
|
82
|
+
{{ row.status === 'on' ? '上架' : '下架' }}
|
|
83
|
+
</tiny-tag>
|
|
84
|
+
</template>
|
|
85
|
+
</tiny-grid-column>
|
|
86
|
+
</tiny-grid>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { ref } from 'vue'
|
|
93
|
+
import productsData from './products.json'
|
|
94
|
+
import { $local } from '../../composable/utils'
|
|
95
|
+
import { useNextServer } from '@opentiny/next-vue'
|
|
96
|
+
import { TinyGrid, TinyGridColumn, TinyButton, TinyTag, TinyModal, TinyImage } from '@opentiny/vue'
|
|
97
|
+
|
|
98
|
+
if (!$local.products) {
|
|
99
|
+
$local.products = productsData
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const products = ref($local.products)
|
|
103
|
+
const gridRef = ref(null)
|
|
104
|
+
|
|
105
|
+
const categoryLabels: Record<string, string> = {
|
|
106
|
+
phones: '手机',
|
|
107
|
+
laptops: '笔记本',
|
|
108
|
+
tablets: '平板'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 新增商品到编辑弹窗
|
|
112
|
+
const addProductToEdit = async () => {
|
|
113
|
+
gridRef?.value?.insert({
|
|
114
|
+
'image': 'https://img1.baidu.com/it/u=1559062020,1043707656&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=500',
|
|
115
|
+
price: 10000,
|
|
116
|
+
stock: 100,
|
|
117
|
+
category: 'phones',
|
|
118
|
+
status: 'on'
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const removeProduct = () => {
|
|
123
|
+
const selectedRows = gridRef?.value?.getSelectRecords()
|
|
124
|
+
if (selectedRows.length === 0) {
|
|
125
|
+
TinyModal.confirm({
|
|
126
|
+
message: '请选择要删除的商品',
|
|
127
|
+
title: '删除商品',
|
|
128
|
+
status: 'warning'
|
|
129
|
+
})
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (selectedRows.length > 0) {
|
|
133
|
+
gridRef?.value?.removeSelecteds()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const saveProduct = () => {
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
const data = gridRef?.value?.getTableData()
|
|
140
|
+
$local.products = data.tableData
|
|
141
|
+
TinyModal.message({
|
|
142
|
+
message: '保存成功',
|
|
143
|
+
status: 'success'
|
|
144
|
+
})
|
|
145
|
+
}, 1000)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { server } = useNextServer({
|
|
149
|
+
serverInfo: { name: 'commodity-config', version: '1.0.0' }
|
|
150
|
+
})
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style scoped lang="less">
|
|
154
|
+
.products-page {
|
|
155
|
+
.page-header {
|
|
156
|
+
display: flex;
|
|
157
|
+
justify-content: space-between;
|
|
158
|
+
align-items: center;
|
|
159
|
+
margin-bottom: 20px;
|
|
160
|
+
padding: 16px 20px;
|
|
161
|
+
background-color: #ffffff;
|
|
162
|
+
border-radius: 8px;
|
|
163
|
+
position: relative;
|
|
164
|
+
border: 1px solid #edf2f7;
|
|
165
|
+
|
|
166
|
+
&::before {
|
|
167
|
+
content: '';
|
|
168
|
+
position: absolute;
|
|
169
|
+
top: 50%;
|
|
170
|
+
left: 0;
|
|
171
|
+
width: 4px;
|
|
172
|
+
height: 24px;
|
|
173
|
+
background: #1677ff;
|
|
174
|
+
border-radius: 2px;
|
|
175
|
+
transform: translateY(-50%);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
h3 {
|
|
179
|
+
margin: 0;
|
|
180
|
+
font-size: 20px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
color: #1f2937;
|
|
183
|
+
position: relative;
|
|
184
|
+
padding-left: 20px;
|
|
185
|
+
letter-spacing: 0.3px;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.button-box {
|
|
191
|
+
display: flex;
|
|
192
|
+
gap: 16px;
|
|
193
|
+
margin-bottom: 20px;
|
|
194
|
+
justify-content: flex-end;
|
|
195
|
+
}
|
|
196
|
+
.loading-state {
|
|
197
|
+
padding: 20px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.product-image {
|
|
201
|
+
width: 40px;
|
|
202
|
+
height: 40px;
|
|
203
|
+
border-radius: 4px;
|
|
204
|
+
}
|
|
205
|
+
.page-content {
|
|
206
|
+
padding: 20px;
|
|
207
|
+
background: #fff;
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03);
|
|
210
|
+
}
|
|
211
|
+
</style>
|