create-jnrs-template-vue 1.1.3 → 1.1.4

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 (32) hide show
  1. package/README.md +5 -5
  2. package/bin/create.mjs +1 -1
  3. package/jnrs-template-vue/README.md +2 -2
  4. package/jnrs-template-vue/components.d.ts +2 -0
  5. package/jnrs-template-vue/package.json +7 -8
  6. package/jnrs-template-vue/public/system/menu.json +8 -4
  7. package/jnrs-template-vue/src/assets/fonts/.keep +0 -0
  8. package/jnrs-template-vue/src/assets/fonts/AlibabaPuHuiTi-Regular.woff2 +0 -0
  9. package/jnrs-template-vue/src/assets/fonts/AlimamaShuHeiTi-Bold.woff2 +0 -0
  10. package/jnrs-template-vue/src/assets/images/common/jnrs-white.svg +1 -0
  11. package/jnrs-template-vue/src/assets/styles/fonts.scss +24 -0
  12. package/jnrs-template-vue/src/assets/styles/init.scss +33 -0
  13. package/jnrs-template-vue/src/assets/styles/main.scss +27 -0
  14. package/jnrs-template-vue/src/assets/styles/root.scss +29 -0
  15. package/jnrs-template-vue/src/layout/BlankLayout.vue +2 -1
  16. package/jnrs-template-vue/src/layout/RouterTabs.vue +11 -7
  17. package/jnrs-template-vue/src/layout/SideMenu.vue +82 -23
  18. package/jnrs-template-vue/src/layout/SideMenuItem.vue +4 -4
  19. package/jnrs-template-vue/src/layout/TopHeader.vue +62 -86
  20. package/jnrs-template-vue/src/layout/index.vue +12 -8
  21. package/jnrs-template-vue/src/main.ts +2 -2
  22. package/jnrs-template-vue/src/router/routes.ts +16 -16
  23. package/jnrs-template-vue/src/views/login/index.vue +228 -38
  24. package/jnrs-template-vue/tsconfig.json +6 -1
  25. package/jnrs-template-vue/vite.config.ts +2 -3
  26. package/jnrs-template-vue/viteMockServe/index.ts +5 -0
  27. package/package.json +1 -1
  28. package/jnrs-template-vue/src/assets/styles/base.css +0 -28
  29. package/jnrs-template-vue/src/assets/styles/main.css +0 -1
  30. package/jnrs-template-vue/src/utils/storage.ts +0 -7
  31. package/jnrs-template-vue/src/utils/validate.ts +0 -321
  32. package/jnrs-template-vue/src/utils/validator.ts +0 -153
@@ -10,8 +10,8 @@ const { avatar } = useAvatar()
10
10
  const { userInfo, asyncClearAuth } = useAuthStore()
11
11
 
12
12
  const systemStore = useSystemStore()
13
- const { menuCollapse, documentFullscreen } = storeToRefs(systemStore)
14
- const { toggleCollapse, toggleFullScreen } = systemStore
13
+ const { documentFullscreen } = storeToRefs(systemStore)
14
+ const { toggleFullScreen } = systemStore
15
15
 
16
16
  const handleLogout = async () => {
17
17
  try {
@@ -29,39 +29,41 @@ const handleLogout = async () => {
29
29
 
30
30
  <template>
31
31
  <div class="topHeader">
32
- <div class="header_left">
32
+ <div class="left">
33
33
  <el-icon
34
- class="headerBtn"
35
- :class="{ sideBtn_active: menuCollapse }"
36
- @click="toggleCollapse()"
34
+ class="btn"
35
+ title="可视化看板"
36
+ @click="
37
+ handleRouter({
38
+ name: 'Visual'
39
+ })
40
+ "
37
41
  >
38
- <Fold />
42
+ <component :is="'Platform'" />
39
43
  </el-icon>
40
- <el-icon class="headerBtn fullBtn" @click="toggleFullScreen()">
44
+ </div>
45
+ <div class="right">
46
+ <el-icon class="btn" title="全屏切换" @click="toggleFullScreen()">
41
47
  <component :is="!documentFullscreen ? 'FullScreen' : 'Rank'" />
42
48
  </el-icon>
43
- </div>
44
- <div class="header_right">
45
49
  <!-- 头像和用户名 -->
46
50
  <el-popover
47
51
  placement="bottom"
48
52
  trigger="click"
49
53
  :teleported="false"
50
- :width="300"
54
+ :width="260"
51
55
  :hide-after="0"
52
56
  >
53
57
  <template #reference>
54
58
  <span class="userMenu_reference">
55
- <img :src="avatar" alt="" />
56
- <span style="margin: 0 10px">
57
- <span>{{ userInfo.name }}</span>
58
- <span class="userMenu_roleName" v-if="userInfo.role">[{{ userInfo.role }}]</span>
59
- </span>
60
- <el-icon><arrow-down /></el-icon>
59
+ <img class="userMenu_avatar" :src="avatar" alt="avatar" />
60
+ <span>{{ userInfo.name }}</span>
61
+ <span class="userMenu_roleName" v-if="userInfo.role">[{{ userInfo.role }}]</span>
62
+ <el-icon class="userMenu_icon"><arrow-down /></el-icon>
61
63
  </span>
62
64
  </template>
63
65
  <div class="userMenu_dropdown">
64
- <img :src="avatar" alt="" />
66
+ <img class="userMenu_dropdown_avatar" :src="avatar" alt="avatar" />
65
67
  <b>
66
68
  {{ userInfo.name }}
67
69
  <span class="userMenu_roleName" v-if="userInfo.role">[{{ userInfo.role }}]</span>
@@ -93,116 +95,90 @@ const handleLogout = async () => {
93
95
  </template>
94
96
 
95
97
  <style lang="scss" scoped>
98
+ $topHoverSize: 35px;
99
+
96
100
  .topHeader {
101
+ position: relative;
102
+ z-index: 15;
97
103
  width: 100%;
98
104
  height: 100%;
99
105
  display: flex;
100
106
  align-items: center;
101
107
  justify-content: space-between;
102
- background: #fff;
103
- padding-left: 0;
104
- z-index: 10;
105
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
106
- :deep(.el-radio-button__inner:hover) {
107
- color: #d15f2c;
108
- }
109
- :deep(.is-active .el-radio-button__inner:hover) {
110
- color: #fff;
111
- }
112
- .headerBtn {
108
+ background: var(--jnrs-card-primary);
109
+ padding: 0 8px;
110
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
111
+
112
+ .btn {
113
+ position: relative;
113
114
  margin: 0 8px;
114
- font-size: 24px;
115
+ font-size: 22px;
115
116
  transition: all 0.25s ease 0s;
116
117
  cursor: pointer;
117
118
  &:hover {
118
- color: #d15f2c;
119
+ color: var(--jnrs-color-primary);
120
+ &::after {
121
+ content: '';
122
+ position: absolute;
123
+ top: 50%;
124
+ left: 50%;
125
+ transform: translate(-50%, -50%);
126
+ width: $topHoverSize;
127
+ height: $topHoverSize;
128
+ background: var(--jnrs-color-primary);
129
+ opacity: 0.1;
130
+ border-radius: 50%;
131
+ }
119
132
  }
120
133
  }
121
- .fullBtn {
122
- font-size: 20px;
123
- }
124
- .sideBtn_active {
125
- transform: rotate(-180deg);
126
- }
127
- .header_left {
128
- display: flex;
129
- align-items: center;
130
- }
131
- .header_right {
134
+
135
+ .left,
136
+ .right {
132
137
  display: flex;
133
138
  align-items: center;
134
- .header_right_item {
135
- display: flex;
136
- align-items: center;
137
- }
138
- .modeLable {
139
- padding-right: 8px;
140
- margin-left: 30px;
141
- color: #d15f2c;
142
- font-size: 14px;
143
- }
144
- }
145
- .hoverBtn {
146
- position: relative;
147
- top: 1px;
148
- font-size: 24px;
149
- margin-left: 30px;
150
- cursor: pointer;
151
- &:hover {
152
- color: #d15f2c;
153
- }
154
139
  }
140
+
155
141
  .userMenu_reference {
156
142
  display: flex;
157
143
  align-items: center;
158
- margin-left: 30px;
159
- font-size: 16px;
160
- color: #424242;
144
+ margin-left: 16px;
161
145
  cursor: pointer;
162
146
  &:hover {
163
- color: #d15f2c;
147
+ color: var(--jnrs-color-primary);
164
148
  }
165
- img {
166
- width: 35px;
167
- height: 35px;
149
+ .userMenu_avatar {
150
+ width: $topHoverSize;
151
+ height: $topHoverSize;
168
152
  border-radius: 50%;
169
- border: 1px solid #ccc;
170
- overflow: hidden;
171
- object-fit: cover;
172
- object-position: center center;
173
- flex-grow: 0;
174
- flex-shrink: 0;
153
+ margin-right: 4px;
154
+ }
155
+ .userMenu_icon {
156
+ margin-left: 4px;
157
+ color: var(--jnrs-font-primary-06);
175
158
  }
176
159
  }
177
160
  .userMenu_dropdown {
178
- padding: 10px;
179
161
  text-align: center;
180
162
  img {
181
163
  width: 80px;
182
164
  height: 80px;
183
- border: 1px solid #ccc;
184
165
  border-radius: 50%;
185
- overflow: hidden;
186
- object-fit: cover;
187
- object-position: center center;
188
166
  }
189
167
  b {
190
168
  display: block;
191
- width: 100%;
192
169
  font-size: 16px;
193
- color: #000;
194
- margin-bottom: 5px;
170
+ color: var(--jnrs-font-primary);
195
171
  }
196
172
  .loginDateTime {
197
- color: #999;
173
+ color: var(--jnrs-font-primary-06);
198
174
  }
199
175
  .userMenu_dropdown_btn {
200
- margin-top: 20px;
176
+ margin-top: 8px;
201
177
  }
202
178
  }
203
179
  .userMenu_roleName {
204
180
  display: inline;
205
- color: #d15f2c;
181
+ color: var(--jnrs-color-primary);
206
182
  }
207
183
  }
208
184
  </style>
@@ -5,10 +5,10 @@ import RouterTabs from './RouterTabs.vue'
5
5
  </script>
6
6
 
7
7
  <template>
8
- <el-container class="layout-container">
8
+ <el-container class="layout_container">
9
9
  <SideMenu />
10
10
  <el-container>
11
- <el-header class="layout-header">
11
+ <el-header class="layout_header">
12
12
  <TopHeader />
13
13
  </el-header>
14
14
  <RouterTabs />
@@ -24,14 +24,18 @@ import RouterTabs from './RouterTabs.vue'
24
24
  </template>
25
25
 
26
26
  <style lang="scss" scoped>
27
- .layout-container {
27
+ .layout_header {
28
28
  width: 100%;
29
- height: 100%;
30
- background: #f2f2f2;
29
+ height: var(--jnrs-head-height);
30
+ padding: 0;
31
31
  }
32
32
 
33
- .layout-header {
34
- height: 50px;
35
- padding: 0;
33
+ .layout_container {
34
+ width: 100%;
35
+ height: 100%;
36
+
37
+ .el-main {
38
+ padding: 10px;
39
+ }
36
40
  }
37
41
  </style>
@@ -1,5 +1,5 @@
1
1
  import 'element-plus/dist/index.css'
2
- import './assets/styles/main.css'
2
+ import './assets/styles/main.scss'
3
3
 
4
4
  import { createApp } from 'vue'
5
5
  import { createPinia } from 'pinia'
@@ -10,7 +10,7 @@ import { router } from './router'
10
10
 
11
11
  const app = createApp(App)
12
12
 
13
- // element-plus 相关
13
+ // element-plus 自动引入所有图标
14
14
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
15
15
  app.component(key, component)
16
16
  }
@@ -10,25 +10,25 @@ export const LAYOUT_NAME = 'Layout'
10
10
  export const GLOBAL_COMPONENT = import('@/layout/BlankLayout.vue')
11
11
 
12
12
  export const routes = [
13
- // {
14
- // path: '/login',
15
- // component: () => import('@/layout/BlankLayout.vue'),
16
- // meta: { title: '登录' },
17
- // children: [
18
- // {
19
- // path: '',
20
- // name: 'Login',
21
- // meta: { title: '登录', noAuth: true },
22
- // component: () => import('@/views/login/index.vue')
23
- // }
24
- // ]
25
- // },
26
13
  {
27
- name: 'Login',
28
14
  path: '/login',
29
- component: () => import('@/views/login/index.vue'),
30
- meta: { title: '登录', noAuth: true, global: true }
15
+ component: () => import('@/layout/BlankLayout.vue'),
16
+ meta: { title: '登录' },
17
+ children: [
18
+ {
19
+ path: '',
20
+ name: 'Login',
21
+ meta: { title: '登录', noAuth: true },
22
+ component: () => import('@/views/login/index.vue')
23
+ }
24
+ ]
31
25
  },
26
+ // {
27
+ // name: 'Login',
28
+ // path: '/login',
29
+ // component: () => import('@/views/login/index.vue'),
30
+ // meta: { title: '登录', noAuth: true, global: true }
31
+ // },
32
32
  {
33
33
  name: LAYOUT_NAME,
34
34
  path: '/',
@@ -1,61 +1,251 @@
1
1
  <script setup lang="ts">
2
- import { reactive, ref } from 'vue'
3
2
  import type { FormInstance, FormRules } from 'element-plus'
3
+ import { ref } from 'vue'
4
4
  import { LoginApi } from '@/api/base/index'
5
5
  import { useAuthStore } from '@/stores'
6
6
  import { handleRouter, useRoute } from '@jnrs/vue-core/router'
7
+ import { isWeakPwd } from '@jnrs/shared/validator'
7
8
 
8
9
  const route = useRoute()
10
+ const loading = ref(false)
9
11
 
12
+ // 表单 ref
13
+ const ruleFormRef = ref<FormInstance>()
10
14
  // 表单数据
11
- const form = reactive({
15
+ const ruleForm = ref({
12
16
  account: '',
13
17
  password: ''
14
18
  })
15
-
16
19
  // 校验规则
17
- const rules = reactive<FormRules>({
20
+ const rules = ref<FormRules>({
18
21
  account: [{ required: true, message: '请输入用户名', trigger: 'change' }],
19
- password: [{ required: true, message: '请输入密码', trigger: 'change' }]
22
+ password: [
23
+ { required: true, message: '请输入密码', trigger: 'change' },
24
+ { validator: isWeakPwd, trigger: 'change' }
25
+ ]
20
26
  })
21
27
 
22
- // 表单 ref
23
- const formRef = ref<FormInstance>()
24
-
25
28
  // 提交函数
26
- const loading = ref(false)
27
- const submitForm = async (formEl: FormInstance | undefined) => {
28
- if (!formEl) return
29
- try {
29
+ const submitForm = () => {
30
+ if (!ruleFormRef.value) return
31
+ ruleFormRef.value.validate(async (valid) => {
32
+ if (!valid) return
30
33
  loading.value = true
31
- await formEl.validate()
32
-
33
- const res = await LoginApi(form)
34
- const { token, dict, ...userInfo } = res
35
-
36
- const { asyncSetAuth } = useAuthStore()
37
- await asyncSetAuth({
38
- token: token,
39
- dict: dict,
40
- userInfo: userInfo
41
- })
42
-
43
- handleRouter({ path: route.query.redirect?.toString() || '/' }, 'replace')
44
- } catch {
45
- } finally {
46
- loading.value = false
47
- }
34
+ try {
35
+ const res = await LoginApi(ruleForm.value)
36
+ const { token, dict, ...userInfo } = res
37
+ const { asyncSetAuth } = useAuthStore()
38
+ await asyncSetAuth({
39
+ token: token,
40
+ dict: dict,
41
+ userInfo: userInfo
42
+ })
43
+ handleRouter({ path: route.query.redirect?.toString() || '/' }, 'replace')
44
+ } catch {
45
+ } finally {
46
+ loading.value = false
47
+ }
48
+ })
48
49
  }
49
50
  </script>
50
51
 
51
52
  <template>
52
- <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
53
- <el-form-item label="用户名" prop="account">
54
- <el-input v-model="form.account" />
55
- </el-form-item>
56
- <el-form-item label="密码" prop="password">
57
- <el-input v-model="form.password" type="password" />
58
- </el-form-item>
59
- </el-form>
60
- <el-button type="primary" :loading="loading" @click="submitForm(formRef)">提交</el-button>
53
+ <div class="main">
54
+ <div class="card">
55
+ <div class="card_left">
56
+ <div class="card_left_mid">
57
+ <img class="logo" src="@/assets/images/common/jnrs-white.svg" alt="JNRS" />
58
+ <h5 class="title">信息化管理系统模板</h5>
59
+ </div>
60
+ </div>
61
+ <div class="card_right">
62
+ <div class="card_right_mid">
63
+ <h1 class="title">登录</h1>
64
+ <el-form
65
+ class="form"
66
+ ref="ruleFormRef"
67
+ :model="ruleForm"
68
+ :rules="rules"
69
+ size="large"
70
+ @keyup.enter="submitForm()"
71
+ >
72
+ <el-form-item prop="account">
73
+ <el-input
74
+ v-model="ruleForm.account"
75
+ placeholder="请输入账号"
76
+ prefix-icon="User"
77
+ clearable
78
+ />
79
+ </el-form-item>
80
+ <el-form-item prop="password">
81
+ <el-input
82
+ v-model="ruleForm.password"
83
+ type="password"
84
+ autocomplete="off"
85
+ placeholder="请输入密码"
86
+ prefix-icon="Lock"
87
+ show-password
88
+ ></el-input>
89
+ </el-form-item>
90
+ <el-form-item>
91
+ <el-button class="btn" type="primary" :loading="loading" @click="submitForm()">
92
+ 登 录
93
+ </el-button>
94
+ </el-form-item>
95
+ </el-form>
96
+ <div class="greeting">欢迎使用</div>
97
+ </div>
98
+ <div class="card_right_copyright">Powered by JNRS TECH 2026</div>
99
+ </div>
100
+ </div>
101
+ </div>
61
102
  </template>
103
+
104
+ <style scoped lang="scss">
105
+ @use 'sass:math';
106
+
107
+ $design-width: 1920; // 设计稿宽度
108
+
109
+ @function px2vw($px) {
110
+ @return math.div($px, $design-width) * 100vw;
111
+ }
112
+
113
+ .main {
114
+ position: relative;
115
+ width: 100%;
116
+ height: 100%;
117
+ background: radial-gradient(circle at center, #232a4c, #000);
118
+
119
+ &::before {
120
+ content: '';
121
+ position: absolute;
122
+ width: 100%;
123
+ height: 100%;
124
+ background-image: radial-gradient(transparent 50%, rgba(255, 255, 255, 0.05) 50%);
125
+ background-size: 10px 10px;
126
+ }
127
+ }
128
+
129
+ .card {
130
+ display: flex;
131
+ align-items: center;
132
+ position: absolute;
133
+ left: 50%;
134
+ top: 50%;
135
+ transform: translate(-50%, -50%);
136
+ width: 1000px;
137
+ height: 500px;
138
+ border-radius: 15px;
139
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
140
+ background: radial-gradient(circle at center, #232a4c, #000);
141
+ overflow: hidden;
142
+
143
+ .card_left {
144
+ display: flex;
145
+ justify-content: center;
146
+ align-items: center;
147
+ width: 50%;
148
+ height: 100%;
149
+ box-shadow: 5px 0 10px rgba(0, 0, 0, 0.5);
150
+ background: url('@/assets/img/common/card_bg.png') no-repeat;
151
+ background-size: 100% 100%;
152
+ text-align: center;
153
+ font-size: 22px;
154
+
155
+ .card_left_mid {
156
+ width: 50%;
157
+
158
+ .logo {
159
+ width: 100%;
160
+ margin-bottom: 20px;
161
+ filter: opacity(0.5);
162
+ }
163
+
164
+ .title {
165
+ font-size: 24px;
166
+ font-weight: normal;
167
+ font-family: AlimamaShuHeiTi-Bold;
168
+ color: rgba(255, 255, 255, 0.5);
169
+ }
170
+ }
171
+ }
172
+
173
+ .card_right {
174
+ width: 50%;
175
+ height: 100%;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ position: relative;
180
+
181
+ &::after {
182
+ content: '';
183
+ position: absolute;
184
+ z-index: -1;
185
+ left: 0;
186
+ top: 0;
187
+ width: 100%;
188
+ height: 100%;
189
+ background: rgba(255, 255, 255, 0.7);
190
+ backdrop-filter: blur(4px);
191
+ -webkit-backdrop-filter: blur(4px);
192
+ }
193
+
194
+ .card_right_mid {
195
+ width: 50%;
196
+
197
+ .title {
198
+ text-align: center;
199
+ color: var(--jnrs-color-primary);
200
+ }
201
+
202
+ .form {
203
+ margin-top: 50px;
204
+ }
205
+
206
+ .btn {
207
+ width: 100%;
208
+ background: var(--jnrs-color-primary);
209
+ border: none;
210
+ transition: all 0.25s ease;
211
+
212
+ &:hover {
213
+ background: var(--jnrs-color-primary-light);
214
+ }
215
+ }
216
+
217
+ .greeting {
218
+ position: relative;
219
+ color: var(--jnrs-color-primary-06);
220
+ font-size: 14px;
221
+ text-align: center;
222
+
223
+ &::before,
224
+ &::after {
225
+ content: '';
226
+ position: absolute;
227
+ right: 0;
228
+ top: 50%;
229
+ width: 35%;
230
+ height: 1px;
231
+ background: var(--jnrs-color-primary-06);
232
+ transform: translateY(-50%);
233
+ filter: opacity(0.25);
234
+ }
235
+
236
+ &::after {
237
+ left: 0;
238
+ top: 50%;
239
+ }
240
+ }
241
+ }
242
+
243
+ .card_right_copyright {
244
+ position: absolute;
245
+ bottom: 5px;
246
+ font-size: 12px;
247
+ color: rgba(0, 0, 0, 0.1);
248
+ }
249
+ }
250
+ }
251
+ </style>
@@ -19,6 +19,11 @@
19
19
  "@/*": ["src/*"]
20
20
  }
21
21
  },
22
- "include": ["src/**/*", "vite.config.ts"],
22
+ "include": [
23
+ "src/**/*",
24
+ "vite.config.ts",
25
+ "../../../packages/shared/src/validate.ts",
26
+ "../../../packages/shared/src/validator.ts"
27
+ ],
23
28
  "exclude": ["node_modules", "dist"]
24
29
  }
@@ -1,5 +1,4 @@
1
1
  import { fileURLToPath, URL } from 'node:url'
2
-
3
2
  import { defineConfig, loadEnv } from 'vite'
4
3
  import vue from '@vitejs/plugin-vue'
5
4
  import AutoImport from 'unplugin-auto-import/vite'
@@ -7,7 +6,7 @@ import Components from 'unplugin-vue-components/vite'
7
6
  import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
8
7
  import compression from 'vite-plugin-compression'
9
8
  import { viteMockServe } from 'vite-plugin-mock'
10
- import vueDevTools from 'vite-plugin-vue-devtools'
9
+ // import vueDevTools from 'vite-plugin-vue-devtools'
11
10
 
12
11
  const path = (url: string) => fileURLToPath(new URL(url, import.meta.url))
13
12
  const config = loadEnv('development', './')
@@ -16,7 +15,7 @@ const isMock = config.VITE_USE_MOCK === 'true'
16
15
  export default defineConfig({
17
16
  plugins: [
18
17
  vue(),
19
- vueDevTools(),
18
+ // vueDevTools(),
20
19
  AutoImport({
21
20
  resolvers: [ElementPlusResolver()]
22
21
  }),
@@ -6,6 +6,11 @@ const res_success = {
6
6
  msg: '操作成功'
7
7
  }
8
8
 
9
+ const res_fail = {
10
+ code: 1,
11
+ msg: '操作失败'
12
+ }
13
+
9
14
  export default [
10
15
  // 获取菜单
11
16
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-jnrs-template-vue",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "巨能前端工程化开发,Vue 项目模板脚手架",
5
5
  "keywords": [
6
6
  "vue",