@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.
Files changed (71) hide show
  1. package/demos/apis/dialog-select.js +42 -0
  2. package/demos/apis/popeditor.js +14 -0
  3. package/demos/apis/steps.js +15 -0
  4. package/demos/apis/text-popup.js +2 -2
  5. package/demos/apis/user-head.js +7 -18
  6. package/demos/mobile-first/app/pager/webdoc/pager.js +0 -48
  7. package/demos/mobile-first/app/steps/vertical.vue +14 -1
  8. package/demos/pc/app/calendar-view/calendar-event.spec.ts +2 -2
  9. package/demos/pc/app/date-panel/basic-usage.spec.ts +6 -6
  10. package/demos/pc/app/date-panel/custom-week.spec.ts +1 -1
  11. package/demos/pc/app/date-panel/disabled-date.spec.ts +8 -9
  12. package/demos/pc/app/date-panel/event.spec.ts +4 -4
  13. package/demos/pc/app/date-panel/format.spec.ts +2 -2
  14. package/demos/pc/app/date-panel/readonly.spec.ts +3 -3
  15. package/demos/pc/app/date-panel/unlink-panels.spec.ts +4 -4
  16. package/demos/pc/app/date-picker/basic-usage.spec.ts +2 -2
  17. package/demos/pc/app/date-picker/date-range.spec.ts +3 -3
  18. package/demos/pc/app/dialog-select/nest-grid-multi-composition-api.vue +19 -1
  19. package/demos/pc/app/dialog-select/nest-grid-multi.spec.ts +19 -0
  20. package/demos/pc/app/dialog-select/nest-grid-multi.vue +17 -1
  21. package/demos/pc/app/dialog-select/webdoc/dialog-select.js +2 -2
  22. package/demos/pc/app/grid/base/basic-usage-composition-api.vue +48 -93
  23. package/demos/pc/app/grid/base/basic-usage.spec.js +1 -1
  24. package/demos/pc/app/grid/base/basic-usage.vue +29 -132
  25. package/demos/pc/app/grid/dynamically-columns/dynamically-columns.spec.js +3 -3
  26. package/demos/pc/app/grid/webdoc/grid-ai-agent.js +23 -0
  27. package/demos/pc/app/popeditor/condition-layout-composition-api.vue +1 -0
  28. package/demos/pc/app/popeditor/condition-layout.spec.ts +1 -0
  29. package/demos/pc/app/popeditor/condition-layout.vue +1 -0
  30. package/demos/pc/app/popeditor/webdoc/popeditor.js +2 -2
  31. package/demos/pc/app/qr-code/style-composition-api.vue +14 -3
  32. package/demos/pc/app/qr-code/style.vue +15 -3
  33. package/demos/pc/app/text-popup/{value.spec.ts → modelValue.spec.ts} +2 -2
  34. package/demos/pc/app/text-popup/webdoc/text-popup.js +8 -8
  35. package/demos/pc/webdoc/changelog.md +461 -451
  36. package/package.json +27 -19
  37. package/playground/App.vue +2 -2
  38. package/src/App.vue +18 -1
  39. package/src/{views/components-doc/components → components}/demo.vue +1 -1
  40. package/src/{views/components-doc/components → components}/float-settings.vue +24 -7
  41. package/src/components/mcp-docs.vue +55 -0
  42. package/src/components/tiny-robot-chat.vue +128 -0
  43. package/src/composable/DifyModelProvider.ts +65 -0
  44. package/src/composable/storage.ts +71 -0
  45. package/src/composable/useTinyRobot.ts +167 -0
  46. package/src/composable/utils.ts +172 -0
  47. package/src/i18n/index.js +5 -2
  48. package/src/main.js +10 -1
  49. package/src/router.js +9 -0
  50. package/src/tools/appData.js +12 -2
  51. package/src/views/components-doc/common.vue +22 -8
  52. package/src/views/comprehensive/Demo.vue +211 -0
  53. package/src/views/comprehensive/index.vue +391 -0
  54. package/src/views/comprehensive/products.json +99 -0
  55. package/src/views/comprehensive/types/index.ts +37 -0
  56. package/src/views/layout/layout.vue +2 -2
  57. package/demos/mobile-first/app/pager/current-change.vue +0 -34
  58. package/demos/mobile-first/app/pager/next-click.vue +0 -34
  59. package/demos/mobile-first/app/pager/prev-click.vue +0 -34
  60. /package/demos/pc/app/text-popup/{clear-value-composition-api.vue → clear-modelValue-composition-api.vue} +0 -0
  61. /package/demos/pc/app/text-popup/{clear-value.spec.ts → clear-modelValue.spec.ts} +0 -0
  62. /package/demos/pc/app/text-popup/{clear-value.vue → clear-modelValue.vue} +0 -0
  63. /package/demos/pc/app/text-popup/{value-composition-api.vue → modelValue-composition-api.vue} +0 -0
  64. /package/demos/pc/app/text-popup/{value.vue → modelValue.vue} +0 -0
  65. /package/src/{views/components-doc/components → components}/anchor.vue +0 -0
  66. /package/src/{views/components-doc/components → components}/api-docs.vue +0 -0
  67. /package/src/{views/components-doc/components → components}/async-highlight.vue +0 -0
  68. /package/src/{views/components-doc/components → components}/contributor.vue +0 -0
  69. /package/src/{views/components-doc/components → components}/header.vue +0 -0
  70. /package/src/{views/components-doc/components → components}/version-tip.vue +0 -0
  71. /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 }
@@ -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 './components/demo.vue'
98
- import AsideAnchor from './components/anchor.vue'
99
- import ComponentHeader from './components/header.vue'
100
- import ComponentContributor from './components/contributor.vue'
101
- import ApiDocs from './components/api-docs.vue'
102
- import useTasksFinish from './composition/useTasksFinish'
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
- const common = new window.TDCommon(['#footer'], { allowDarkTheme: true })
170
- common.renderFooter()
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>