├── .gitignore
├── .npmrc
├── .vscode
└── settings.json
├── README.md
├── README
├── image-20230716135559277.png
├── image-20230716135635973.png
├── image-20230716135655742.png
└── image-20230716135712479.png
├── app.vue
├── assets
├── images
│ └── sponsor
│ │ ├── alipay.jpg
│ │ └── wepay.jpg
└── scss
│ ├── common.scss
│ └── element
│ ├── dark.scss
│ └── index.scss
├── components
├── applications
│ ├── list.vue
│ └── list
│ │ ├── func.vue
│ │ └── item.vue
├── database
│ ├── detail.vue
│ ├── edit.vue
│ ├── fields.vue
│ ├── history.vue
│ ├── list.vue
│ ├── list
│ │ └── item.vue
│ ├── response.vue
│ ├── run.vue
│ └── table.vue
├── layout
│ ├── drawer.vue
│ ├── footer.vue
│ ├── header.vue
│ ├── loading.vue
│ ├── panel.vue
│ └── twoColumn.vue
├── manager
│ ├── create.vue
│ ├── detail.vue
│ ├── edit.vue
│ ├── header.vue
│ ├── history.vue
│ ├── runHistory.vue
│ ├── side.vue
│ ├── tab.vue
│ └── tmp.vue
└── scrollBox.vue
├── composables
├── cloud.js
└── request.js
├── layouts
├── default.vue
└── manager.vue
├── middleware
└── auth.global.ts
├── nuxt.config.ts
├── package.json
├── pages
├── index.vue
├── index
│ ├── [...key].vue
│ ├── index.vue
│ └── query.vue
├── old
│ ├── app
│ │ ├── [appid].vue
│ │ └── [appid]
│ │ │ └── database.vue
│ └── index.vue
└── welcome.vue
├── public
├── favicon.ico
├── loading.html
└── texture.png
├── server
└── tsconfig.json
├── stores
├── app.ts
├── collection.ts
├── config.ts
├── document.ts
├── field.ts.bak
├── query.ts
├── tab.ts
└── user.ts
├── tsconfig.json
├── uno.config.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .nuxt
4 | .nitro
5 | .cache
6 | dist
7 |
8 | # Node dependencies
9 | node_modules
10 |
11 | # Logs
12 | logs
13 | *.log
14 |
15 | # Misc
16 | .DS_Store
17 | .fleet
18 | .idea
19 |
20 | # Local env files
21 | .env
22 | .env.*
23 | !.env.example
24 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 | strict-peer-dependencies=false
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "vue.codeActions.enabled": false
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laf curd
2 |
3 | 一个 laf 的数据库管理工具。
4 |
5 | - 访问 https://curd.muyi.dev/ 直接使用。
6 | - 源码在这里 [NMTuan/laf_curd (github.com)](https://github.com/NMTuan/laf_curd)。
7 |
8 | ## 介绍
9 |
10 | 从名字不难看出,这是一个为 laf 云数据库设计的增删改查工具。
11 |
12 | - 前端使用 `Nuxt v3` `Pinia` `Unocss`
13 | - 后端对接 `laf api` `laf-client-sdk`
14 |
15 | 由于没有直接使用 UI 库,所以界面不咋好看。
16 |
17 | 为符合 MVP 最小可行性产品的方案,刚刚完成了 CURD 我就发了第一版 v0.1.0。
18 |
19 | ## 使用
20 |
21 | 首次打开后,是下面这么个样子,`api url` 就是你项目所在的 laf 地址,如果是 laf.dev 就修改一下。然后填写 `pat` 参数即可([如何创建pat?](https://doc.laf.run/guide/cli/#登录))。
22 |
23 | 
24 |
25 | 登录后是下面这个样子,左侧是主菜单,右侧是内容区域。值得说一句的是这次用了多tabs的模式。这也算是管理平台的标配了。
26 |
27 | 
28 |
29 | 下面这个是自定义查询界面,复杂的查询语句都可以在这里尝试。历史语句都保存在浏览器缓存中,放心使用。
30 |
31 | 这里支持 laf 的查询语句,支持 `_` 关键字。
32 |
33 | 
34 |
35 | ### 如何查询?
36 |
37 | > ```
38 | > cloud.database().collection("user").where({ age: _.gt(18) }.get()
39 | > ```
40 |
41 | 我们可以在左侧点击 `user`集合,然后数据框中填写 `where({ age: _.gt(18) }).get()` 点击前方的 `▶`即可。
42 |
43 | > 注意:这里是支持`_`关键字的。具体查询语句可以参考[官方手册](https://doc.laf.run/guide/db/find.html)。
44 |
45 | 除了自定义查询外,我们也可以直接管理每个数据集合,如下图。创建、详情、编辑、删除、筛选,一应俱全。同样筛选记录会保存在浏览器缓存中。这里的查询支持:数据id、 laf where 或者 mongodb where 。
46 |
47 | 
48 |
49 | ## 感谢
50 |
51 | 最后,感谢丰富的前端生态,感谢 laf 这个牛逼的产品。
52 |
53 | - [Nuxt](https://github.com/nuxt/nuxt) MIT
54 | - [Pinia](https://github.com/vuejs/pinia) MIT
55 | - [Unocss](https://github.com/unocss/unocss) MIT
56 | - [Laf](https://github.com/labring/laf) Apache License 2.0
57 |
58 | 如果你也喜欢 laf curd,或者本项目对你有所帮助。
59 |
60 | 可以[来这里点点 star🌟](https://github.com/NMTuan/laf_curd)。也或者[给作者加个鸡腿🍗🍗🍗](https://www.muyi.dev/sponsor)!
61 |
--------------------------------------------------------------------------------
/README/image-20230716135559277.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/README/image-20230716135559277.png
--------------------------------------------------------------------------------
/README/image-20230716135635973.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/README/image-20230716135635973.png
--------------------------------------------------------------------------------
/README/image-20230716135655742.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/README/image-20230716135655742.png
--------------------------------------------------------------------------------
/README/image-20230716135712479.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/README/image-20230716135712479.png
--------------------------------------------------------------------------------
/app.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
26 |
34 |
--------------------------------------------------------------------------------
/assets/images/sponsor/alipay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/assets/images/sponsor/alipay.jpg
--------------------------------------------------------------------------------
/assets/images/sponsor/wepay.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/assets/images/sponsor/wepay.jpg
--------------------------------------------------------------------------------
/assets/scss/common.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/assets/scss/common.scss
--------------------------------------------------------------------------------
/assets/scss/element/dark.scss:
--------------------------------------------------------------------------------
1 | @forward 'element-plus/theme-chalk/src/dark/var.scss' with (
2 | $bg-color: (
3 | 'page': #0a0a0a,
4 | 'overlay': #1d1e1f
5 | )
6 | );
7 |
--------------------------------------------------------------------------------
/assets/scss/element/index.scss:
--------------------------------------------------------------------------------
1 | $-colors: (
2 | 'primary': (
3 | 'base': #14b8a6
4 | ),
5 | // 'success': (
6 | // 'base': #67c23a
7 | // ),
8 | // 'warning': (
9 | // 'base': #e6a23c
10 | // ),
11 | // 'danger': (
12 | // 'base': #f56c6c
13 | // ),
14 | // 'error': (
15 | // 'base': #f56c6c
16 | // ),
17 | // 'info': (
18 | // 'base': #909399
19 | // )
20 | );
21 |
22 | @forward 'element-plus/theme-chalk/src/common/var.scss' with (
23 | $colors: $-colors
24 | );
25 |
26 | // @use './dark.scss';
27 |
--------------------------------------------------------------------------------
/components/applications/list.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
28 |
--------------------------------------------------------------------------------
/components/applications/list/func.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | 云函数
13 | 数据库
18 | 访问策略
19 | 云存储
20 |
21 |
22 |
31 |
32 |
46 |
--------------------------------------------------------------------------------
/components/applications/list/item.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | {{ data.name }} (appId: {{ data.appid }})
14 |
15 |
16 |
17 |
18 |
26 |
--------------------------------------------------------------------------------
/components/database/detail.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | {{ key }} |
16 |
17 |
18 | {{ JSON.stringify(val, null, 2) }} |
19 | {{ val }} |
20 |
21 |
22 |
23 |
24 |
25 |
33 |
47 |
--------------------------------------------------------------------------------
/components/database/edit.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
{{ submitData }}
23 |
25 |
26 |
27 |
28 |
29 |
81 |
96 |
--------------------------------------------------------------------------------
/components/database/fields.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 | {{ row[column.rawColumnKey] }}
24 |
25 |
27 |
28 |
29 |
31 |
32 |
33 |
35 |
36 |
37 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
49 |
50 |
52 |
54 | 居左
55 | 居中
56 | 居右
57 |
58 |
60 | 不浮
61 | 左浮
62 | 右浮
63 |
64 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 新增字段
75 |
76 | 取消
77 |
78 | 提交
79 |
80 |
81 |
82 |
83 |
84 |
185 |
--------------------------------------------------------------------------------
/components/database/history.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ new Date(item.date).toLocaleString() }}
5 |
{{ item.statement }}
7 |
8 |
9 |
10 |
11 |
25 |
--------------------------------------------------------------------------------
/components/database/list.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
18 |
19 |
31 |
--------------------------------------------------------------------------------
/components/database/list/item.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
{{ data.name }}
6 |
7 |
8 |
9 |
21 |
43 |
--------------------------------------------------------------------------------
/components/database/response.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | {{ queryStore.response.message }}
16 |
17 |
18 |
19 |
20 | {{ queryStore.response.message }}
21 |
22 |
23 |
24 |
{{ queryStore.response }}
25 |
26 |
27 |
28 |
32 |
40 |
--------------------------------------------------------------------------------
/components/database/run.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
17 |
19 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
87 |
--------------------------------------------------------------------------------
/components/database/table.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | |
10 | {{ item }} |
11 |
12 |
13 |
14 |
15 |
16 | 【详情】
17 | 【编辑】
18 | 【删除】
19 | |
20 | {{ item[field] }} |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
67 |
110 |
--------------------------------------------------------------------------------
/components/layout/drawer.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
16 |
17 |
39 |
62 |
--------------------------------------------------------------------------------
/components/layout/footer.vue:
--------------------------------------------------------------------------------
1 |
2 | @
3 |
4 |
--------------------------------------------------------------------------------
/components/layout/header.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | laf CURD
16 |
17 |
18 | github
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/components/layout/loading.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
14 |
15 |
23 |
--------------------------------------------------------------------------------
/components/layout/panel.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
39 |
51 |
--------------------------------------------------------------------------------
/components/layout/twoColumn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/components/manager/create.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | submit
15 |
16 |
17 |
18 |
55 |
70 |
--------------------------------------------------------------------------------
/components/manager/detail.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | {{ val }}
15 | {{ val }}
16 |
17 |
18 |
19 |
20 |
21 |
37 |
46 |
--------------------------------------------------------------------------------
/components/manager/edit.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 | update
15 |
16 |
17 |
18 |
78 |
85 |
--------------------------------------------------------------------------------
/components/manager/header.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
29 |
30 | [
31 |
32 | {{ username }}
33 | Sign out
34 | ]
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |

44 |
45 |
46 |

47 |
48 |
49 |
50 |
51 |
52 |
110 |
121 |
--------------------------------------------------------------------------------
/components/manager/history.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
15 |
17 | {{ item.statement }}
18 |
19 |
20 |
21 |
22 |
23 |
43 |
--------------------------------------------------------------------------------
/components/manager/runHistory.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
15 |
17 |
18 |
19 | {{ item.statement }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
46 |
--------------------------------------------------------------------------------
/components/manager/side.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
14 |
15 |
16 |
17 |
18 | {{ data.name }}
19 |
20 |
23 |
24 |
26 |
27 |
28 |
{{ data.name }}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
122 |
157 |
--------------------------------------------------------------------------------
/components/manager/tab.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ appName(item.params.key[0]) }}
18 |
19 |
{{ item.params.key[1] }}
20 |
21 | {{ item.name.replace('index-', '') }}
22 |
23 |
24 |
25 |
26 |
27 |
46 |
118 |
--------------------------------------------------------------------------------
/components/manager/tmp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ i }} | {{ j }}
5 |
6 |
7 |
8 |
16 |
--------------------------------------------------------------------------------
/components/scrollBox.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
23 |
--------------------------------------------------------------------------------
/composables/cloud.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-13 10:34:44
5 | * @LastEditTime: 2023-08-13 11:39:20
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\composables\cloud.js
9 | */
10 | import { Cloud } from 'laf-client-sdk'
11 |
12 | export const useCloud = (payload) => {
13 | const route = useRoute()
14 | const configStore = useConfigStore()
15 | const useStore = useUserStore()
16 | // const [appid, collectionName] = route.params.key
17 | const appid = payload?.appid || route.params.key[0]
18 | const collectionName = payload?.collectionName || route.params.key[1]
19 | const cloud = new Cloud({
20 | baseUrl: `${configStore.apiUrl}`,
21 | dbProxyUrl: `/v1/apps/${appid}/databases/proxy`,
22 | getAccessToken: () => {
23 | return useStore.token
24 | }
25 | })
26 | const collection = cloud.database().collection(collectionName)
27 | const _ = cloud.database().command
28 |
29 | // 根据query条件获取数据列表
30 | const fetch = ({ query, page, pageSize }, loading) => {
31 | return new Promise((resolve, reject) => {
32 | if (loading) {
33 | loading.value = true
34 | }
35 | query = query || {}
36 | page = page || 1
37 | pageSize = pageSize || 10
38 | collection
39 | .where(query)
40 | .skip((page - 1) * pageSize)
41 | .limit(pageSize)
42 | .get()
43 | .then((res) => {
44 | resolve(res)
45 | })
46 | .catch((error) => {
47 | reject(error)
48 | })
49 | .finally(() => {
50 | if (loading) {
51 | loading.value = false
52 | }
53 | })
54 | })
55 | }
56 |
57 | // 根据query条件获取数据总条数
58 | const count = ({ query }, loading) => {
59 | return new Promise((resolve, reject) => {
60 | if (loading) {
61 | loading.value = true
62 | }
63 | collection
64 | .where(query || {})
65 | .count()
66 | .then((res) => {
67 | resolve(res)
68 | })
69 | .catch((error) => {
70 | reject(error)
71 | })
72 | .finally(() => {
73 | if (loading) {
74 | loading.value = false
75 | }
76 | })
77 | })
78 | }
79 |
80 | // 查询一条数据
81 | const fetchOne = ({ query }, loading) => {
82 | return new Promise((resolve, reject) => {
83 | if (loading) {
84 | loading.value = true
85 | }
86 | query = query || {}
87 | collection
88 | .where(query)
89 | .getOne()
90 | .then((res) => {
91 | resolve(res)
92 | })
93 | .catch((error) => {
94 | reject(error)
95 | })
96 | .finally(() => {
97 | if (loading) {
98 | loading.value = false
99 | }
100 | })
101 | })
102 | }
103 |
104 | // 更新数据
105 | const update = ({ id, payload }, loading) => {
106 | return new Promise((resolve, reject) => {
107 | if (!id) {
108 | reject()
109 | }
110 | if (loading) {
111 | loading.value = true
112 | }
113 | payload = payload || {}
114 | collection
115 | .where({ _id: id })
116 | .update(payload, { merge: false })
117 | .then((res) => {
118 | resolve(res)
119 | })
120 | .catch((error) => {
121 | reject(error)
122 | })
123 | .finally(() => {
124 | if (loading) {
125 | loading.value = false
126 | }
127 | })
128 | })
129 | }
130 |
131 | // 删除数据
132 | const remove = (id) => {
133 | return new Promise((resolve, reject) => {
134 | if (!id) {
135 | reject()
136 | }
137 | ElMessageBox.confirm(
138 | 'Are you sure you want to "remove" item ?',
139 | 'Warning',
140 | {
141 | cancelButtonClass: 'is-text',
142 | confirmButtonClass: 'el-button--danger',
143 | beforeClose: async (action, ctx, done) => {
144 | if (action !== 'confirm') {
145 | ctx.confirmButtonLoading = false
146 | done()
147 | return
148 | }
149 | ctx.confirmButtonLoading = true
150 | collection
151 | .doc(id)
152 | .remove()
153 | .then((res) => {
154 | done()
155 | ElMessage({
156 | message: 'remove success',
157 | type: 'success'
158 | })
159 |
160 | resolve(res)
161 | })
162 | .catch((error) => {
163 | ElMessage({
164 | message: 'err',
165 | type: 'error'
166 | })
167 | reject(error)
168 | })
169 | .finally(() => {
170 | ctx.confirmButtonLoading = false
171 | })
172 | }
173 | }
174 | )
175 | .then((action) => {
176 | resolve(action)
177 | })
178 | .catch((action) => {
179 | reject(action)
180 | })
181 | })
182 | }
183 |
184 | // 添加数据
185 | const create = (payload, loading) => {
186 | return new Promise((resolve, reject) => {
187 | loading.value = true
188 | collection
189 | .add(payload)
190 | .then((res) => {
191 | ElMessage({
192 | message: 'created success',
193 | type: 'success'
194 | })
195 |
196 | resolve(res)
197 | })
198 | .catch((error) => {
199 | ElMessage({
200 | message: 'err',
201 | type: 'error'
202 | })
203 | reject(error)
204 | })
205 | .finally(() => {
206 | loading.value = false
207 | })
208 | })
209 | }
210 |
211 | // 执行语句
212 | const run = (statement, loading) => {
213 | return new Promise((resolve, reject) => {
214 | if (loading) {
215 | loading.value = true
216 | }
217 | try {
218 | eval(`collection.${statement}`)
219 | .then((res) => {
220 | resolve(res)
221 | })
222 | .catch((error) => {
223 | reject(error)
224 | })
225 | .finally(() => {
226 | if (loading) {
227 | loading.value = false
228 | }
229 | })
230 | } catch (error) {
231 | ElMessage({
232 | message: error.message,
233 | type: 'error'
234 | })
235 | if (loading) {
236 | loading.value = false
237 | }
238 | reject(error)
239 | }
240 | })
241 | }
242 |
243 | // 获取字段配置
244 | const getFieldConfig = () => {
245 | return new Promise((resolve, reject) => {
246 | cloud
247 | .database()
248 | .collection('lafDB_fields')
249 | .where({
250 | collectionName
251 | })
252 | .getOne()
253 | .then((res) => {
254 | resolve(res)
255 | })
256 | .catch((error) => {
257 | reject(error)
258 | })
259 | })
260 | }
261 | const updateFieldConfig = (id, data) => {
262 | return new Promise((resolve, reject) => {
263 | if (!id) {
264 | cloud
265 | .database()
266 | .collection('lafDB_fields')
267 | .add({
268 | collectionName,
269 | columns: data
270 | })
271 | .then((res) => {
272 | resolve(res)
273 | })
274 | .catch((err) => {
275 | reject(err)
276 | })
277 | } else {
278 | cloud
279 | .database()
280 | .collection('lafDB_fields')
281 | .doc(id)
282 | .update({
283 | columns: data
284 | })
285 | .then((res) => {
286 | resolve(res)
287 | })
288 | .catch((err) => {
289 | reject(err)
290 | })
291 | }
292 | })
293 |
294 | // const id = data._id
295 | // delete data._id
296 | // return new Promise((resolve, reject) => {
297 | // cloud
298 | // .database()
299 | // .collection('lafDB_fields')
300 | // .doc(id)
301 | // .update(data)
302 | // .then((res) => {
303 | // console.log('x', res)
304 | // resolve(res)
305 | // })
306 | // .catch((error) => {
307 | // reject(error)
308 | // })
309 | // })
310 | }
311 |
312 | return {
313 | _,
314 | collection,
315 | collectionName,
316 | cloud,
317 | fetch,
318 | count,
319 | fetchOne,
320 | update,
321 | remove,
322 | create,
323 | run,
324 | getFieldConfig,
325 | updateFieldConfig
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/composables/request.js:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-06-27 15:59:25
5 | * @LastEditTime: 2023-07-11 11:39:41
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\composables\request.js
9 | */
10 | export const request = (params) => {
11 | // let loading
12 | // const runtimeConfig = useRuntimeConfig()
13 | const userStore = useUserStore()
14 | const configStore = useConfigStore()
15 |
16 | const defaultParams = {
17 | url: undefined,
18 | method: 'GET',
19 | path: undefined,
20 | query: {},
21 | body: {},
22 | auth: true
23 | }
24 |
25 | params = { ...defaultParams, ...params }
26 |
27 | // 如果传了url则使用,否则用env中配置项。
28 | const url = (params.url || configStore.apiUrl) + params.path
29 | delete params.url
30 | delete params.path
31 |
32 | // get请求不需要body
33 | if (params.method === 'GET') {
34 | delete params.body
35 | }
36 |
37 | // 鉴权
38 | if (params.auth !== false) {
39 | params.headers = {
40 | Authorization: 'Bearer ' + userStore.token
41 | }
42 | }
43 | delete params.auth
44 |
45 | return new Promise((resolve, reject) => {
46 | if (!url) {
47 | reject({
48 | code: 40000,
49 | message: 'no url'
50 | })
51 | }
52 | $fetch(url, params)
53 | .then((res) => {
54 | if (res instanceof Blob) {
55 | resolve(res)
56 | } else if (!res.error) {
57 | resolve(res)
58 | } else {
59 | alert(res.error)
60 | reject(res)
61 | }
62 | })
63 | .catch((err) => {
64 | if (err.response.status === 401) {
65 | alert('登录超时,请重新登录')
66 | navigateTo({name: 'welcome'})
67 | }else if (err.response) {
68 | alert(JSON.stringify(err.response.data, null, 2))
69 | }
70 | reject(err)
71 | })
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/layouts/manager.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/middleware/auth.global.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-06-29 21:14:49
5 | * @LastEditTime: 2023-07-15 20:46:57
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\middleware\auth.global.ts
9 | */
10 | export default defineNuxtRouteMiddleware((to, from) => {
11 | const userStore = useUserStore()
12 | const configStore = useConfigStore()
13 | const tabStore = useTabStore()
14 |
15 | // 不在白名单, 而且没token
16 | if (!userStore.whiteList.includes(to.path) && !userStore.token) {
17 | return navigateTo('/welcome')
18 | }
19 |
20 | // 随时切换 appid
21 | if (to.params.appid) {
22 | configStore.$patch({ appid: to.params.appid.toString() })
23 | }
24 |
25 | // tabs
26 | // console.log('to', to.name, to.params.key, /^index.*$/.test(to.name))
27 | // if (to.name === 'index-key') {
28 | // tabStore.append(to.params.key.join('/'))
29 | // } else if (/^index.*$/.test(to.name)) {
30 | // tabStore.append(to.params.name)
31 | // }
32 | // console.log(tabStore.list)
33 |
34 | if (!userStore.whiteList.includes(to.path)) {
35 | tabStore.append({
36 | name: to.name,
37 | params: to.params
38 | })
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-06-30 19:16:42
5 | * @LastEditTime: 2023-07-13 14:42:57
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\nuxt.config.ts
9 | */
10 | // https://nuxt.com/docs/api/configuration/nuxt-config
11 | export default defineNuxtConfig({
12 | ssr: false,
13 | app: {
14 | head: {
15 | script: [
16 | {
17 | src: 'https://hm.baidu.com/hm.js?c7a27417fb3ca4a77c2486d1d1d51a0c'
18 | }
19 | ]
20 | }
21 | },
22 | css: [
23 | '@unocss/reset/normalize.css',
24 | '@/assets/scss/common.scss',
25 | 'simplebar-vue/dist/simplebar.min.css'
26 | ],
27 | modules: ['@unocss/nuxt', '@pinia/nuxt', '@element-plus/nuxt'],
28 | devtools: { enabled: false },
29 | runtimeConfig: {
30 | public: {
31 | requestUrl: 'https://api.laf.run',
32 | pat: ''
33 | }
34 | },
35 | experimental: {
36 | viewTransition: true
37 | },
38 | imports: {
39 | dirs: ['stores']
40 | },
41 | vite: {
42 | css: {
43 | preprocessorOptions: {
44 | scss: {
45 | additionalData: `@use "@/assets/scss/element/index.scss" as element;`
46 | }
47 | }
48 | }
49 | },
50 | elementPlus: {
51 | // icon: 'ElIcon',
52 | importStyle: 'scss'
53 | // themes: ['dark']
54 | },
55 | // spaLoadingTemplate: 'public/loading.html'
56 | spaLoadingTemplate: false
57 | })
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nuxt-app",
3 | "private": true,
4 | "scripts": {
5 | "build": "nuxt build",
6 | "dev": "nuxt dev",
7 | "generate": "nuxt generate",
8 | "preview": "nuxt preview",
9 | "postinstall": "nuxt prepare"
10 | },
11 | "devDependencies": {
12 | "@element-plus/nuxt": "^1.0.5",
13 | "@iconify-json/ri": "^1.1.10",
14 | "@nuxt/devtools": "^0.6.7",
15 | "@types/node": "^18",
16 | "@unocss/nuxt": "^0.53.4",
17 | "element-plus": "^2.3.7",
18 | "sass": "^1.63.6"
19 | },
20 | "dependencies": {
21 | "@pinia/nuxt": "^0.4.11",
22 | "ejson-shell-parser": "^1.2.4",
23 | "laf-client-sdk": "^1.0.0-beta.8",
24 | "nuxt": "^3.6.5",
25 | "pinia": "^2.1.4",
26 | "simplebar-vue": "^2.3.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/pages/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Laf x DB
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/pages/index/[...key].vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Create
5 |
6 | Detail
7 | Edit
8 | Delete
9 |
10 |
11 |
13 |
14 | Search ( Enter
15 | )
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
34 |
{{ rowData[column.key] }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
226 |
253 |
--------------------------------------------------------------------------------
/pages/index/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
13 | Laf x DB
14 |
15 |
16 |
--------------------------------------------------------------------------------
/pages/index/query.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 |
25 |
26 |
27 | Run ( Ctrl+Enter | Alt+Enter )
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Detail
37 | Edit
38 | Delete
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{ val }}
46 | {{ val }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
57 |
58 |
60 |
{{ rowData[column.title] }}
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
73 |
74 |
75 |
76 |
261 |
302 |
--------------------------------------------------------------------------------
/pages/old/app/[appid].vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
--------------------------------------------------------------------------------
/pages/old/app/[appid]/database.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
--------------------------------------------------------------------------------
/pages/old/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/pages/welcome.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
Laf x DB
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Sign In
24 |
25 |
26 |
27 |
30 |
31 |
32 |
33 |
67 |
72 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/public/favicon.ico
--------------------------------------------------------------------------------
/public/loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | loading
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NMTuan/laf_curd/9e12df82e6183dc7d555c9eec234f94318eb882e/public/texture.png
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/stores/app.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-01 23:01:05
5 | * @LastEditTime: 2023-07-02 14:35:48
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\app.ts
9 | */
10 | import { defineStore } from 'pinia'
11 |
12 | interface App {
13 | [key: string]: any
14 | }
15 |
16 | export const useAppStore = defineStore('useAppStore', () => {
17 | // 列表
18 | const list: Ref = ref([])
19 |
20 | // 获取列表
21 | const fetch = () => {
22 | return new Promise((resolve, reject) => {
23 | request({
24 | path: '/v1/applications'
25 | })
26 | .then((res) => {
27 | list.value = res.data
28 | resolve(res)
29 | })
30 | .catch((err) => {
31 | reject(err)
32 | })
33 | })
34 | }
35 | return {
36 | list,
37 | fetch
38 | }
39 | })
40 |
--------------------------------------------------------------------------------
/stores/collection.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-01 22:51:38
5 | * @LastEditTime: 2023-07-02 14:17:09
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \project\laf_curd\stores\collection.ts
9 | */
10 | import { defineStore } from 'pinia'
11 |
12 | export const useCollectionStore = defineStore('useCollectionStore', () => {
13 | // 列表
14 | const list = ref([])
15 |
16 | // 获取列表
17 | const fetch = (appid) => {
18 | const configStore = useConfigStore()
19 | return new Promise((resolve, reject) => {
20 | request({
21 | path: `/v1/apps/${configStore.appid || appid}/collections`
22 | })
23 | .then((res) => {
24 | list.value = res.data
25 | resolve(res)
26 | })
27 | .catch((err) => {
28 | reject(err)
29 | })
30 | })
31 | }
32 |
33 | return {
34 | list,
35 | fetch
36 | }
37 | })
38 |
--------------------------------------------------------------------------------
/stores/config.ts:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useConfigStore = defineStore('useConfigStore', () => {
4 | const appid = ref('')
5 | const apiUrl = useCookie('laf_curd_apiUrl', {
6 | default: () => 'https://api.laf.run'
7 | })
8 | const pat = useCookie('laf_curd_pat')
9 |
10 | return {
11 | appid,
12 | apiUrl,
13 | pat
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/stores/document.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-13 10:03:50
5 | * @LastEditTime: 2023-07-13 10:04:15
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\document.ts
9 | */
10 | import { defineStore } from 'pinia'
11 | export const useDocumentStore = defineStore('useDocumentStore', () => {
12 | // 列表
13 |
14 | // 获取列表
15 | const fetch = (appid, collection) => {
16 | const configStore = useConfigStore()
17 | return new Promise((resolve, reject) => {
18 | request({
19 | path: `/v1/apps/${configStore.appid || appid}/collections`
20 | })
21 | .then((res) => {
22 | list.value = res.data
23 | resolve(res)
24 | })
25 | .catch((err) => {
26 | reject(err)
27 | })
28 | })
29 | }
30 |
31 | return {
32 | fetch
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/stores/field.ts.bak:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-08-10 07:29:28
5 | * @LastEditTime: 2023-08-10 21:14:10
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\field.ts
9 | */
10 | import { defineStore } from 'pinia'
11 |
12 | interface Field {
13 | dataKey: string
14 | _id: string
15 | collectionName: string
16 | width: number
17 | key: string
18 | title: string
19 | }
20 |
21 | export const useFieldStore = defineStore('useFieldStore', () => {
22 | const list: Ref = useCookie('lafDB_fields', {
23 | default: () => []
24 | })
25 |
26 | // 更新字段, force 是否强制更新
27 | const updateFields = (fields: Field[], collectionName: string) => {
28 | fields.forEach((field) => {
29 | updateField(field, collectionName)
30 | })
31 | }
32 | const updateField = (field: Field, collectionName: string) => {
33 | const index = list.value.findIndex(
34 | (col) =>
35 | col.key === field.key && col.collectionName === collectionName
36 | )
37 | if (index !== -1 && collectionName) {
38 | // 如果本地存在,则更新
39 | list.value[index] = {
40 | ...list.value[index],
41 | ...field
42 | }
43 | } else {
44 | // 否则插入
45 | list.value.push({
46 | ...{ dataKey: field.key },
47 | ...field
48 | })
49 | }
50 | }
51 |
52 | const getFields = async () => {
53 | const { cloud, collectionName } = useCloud()
54 | const res = await cloud
55 | .database()
56 | .collection('lafDB_fields')
57 | .where({
58 | collectionName
59 | })
60 | .get()
61 | if (res.ok && res.data && Array.isArray(res.data)) {
62 | updateFields(res.data as [], collectionName)
63 | }
64 | }
65 | return {
66 | list,
67 | getFields
68 | }
69 | })
70 |
--------------------------------------------------------------------------------
/stores/query.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-01 17:16:50
5 | * @LastEditTime: 2023-07-15 15:38:16
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\query.ts
9 | */
10 | import { defineStore } from 'pinia'
11 | interface History {
12 | statement: string
13 | date: number
14 | }
15 |
16 | export const useQueryStore = defineStore('useQueryStore', () => {
17 | // 当前实例
18 | const appid = ref('')
19 | // 当前集合
20 | const collection = ref({})
21 | // 查询语句
22 | const statement = ref('get()')
23 | const history: Ref = ref([])
24 | // cloud 实例
25 | const cloud = ref()
26 | const updateCloud = (newCloud) => {
27 | cloud.value = newCloud
28 | }
29 | // 查询
30 | const query = () => {
31 | return new Promise((resolve, reject) => {
32 | if (!collection.value.name) {
33 | response.value = {
34 | ok: false,
35 | message: '请在左侧选择您要查询的集合'
36 | }
37 | return resolve('')
38 | }
39 | try {
40 | const _ = cloud.value.database().command
41 | eval(
42 | `cloud.value.database().collection('${collection.value.name}').${statement.value}`
43 | )
44 | .then((res) => {
45 | response.value = res
46 | addHistory()
47 | resolve('')
48 | })
49 | .catch((err) => {
50 | if (err.response) {
51 | response.value = {
52 | ok: false,
53 | message: err.response.data.code,
54 | data: err.response.data.error
55 | }
56 | } else {
57 | response.value = {
58 | ok: false,
59 | message: err.message
60 | }
61 | }
62 | resolve('')
63 | })
64 | } catch (err) {
65 | response.value = {
66 | ok: false,
67 | message: err.message
68 | }
69 | resolve('')
70 | }
71 | })
72 | }
73 | // 查询结果
74 | const response = ref({})
75 | // 手工更新查询结果
76 | const updateResponse = (val: object) => {
77 | response.value = val
78 | }
79 | // 插入历史
80 | const addHistory = () => {
81 | history.value = history.value.filter((item) => {
82 | return item.statement !== statement.value
83 | })
84 | history.value.push({
85 | statement: statement.value,
86 | date: Date.now()
87 | })
88 | }
89 |
90 | // 切换appid的时候,清理暂存信息
91 | const clear = () => {
92 | collection.value = {}
93 | response.value = {}
94 | }
95 |
96 | const updateById = (payload: any) => {
97 | return new Promise(async (resolve, reject) => {
98 | const id = payload._id
99 | if (!id) {
100 | alert('no find _id')
101 | return resolve('')
102 | }
103 | delete payload._id
104 | await cloud.value
105 | .database()
106 | .collection(collection.value.name)
107 | .doc(id)
108 | .update(payload)
109 | await query()
110 | resolve('')
111 | })
112 | }
113 |
114 | // 删除
115 | const removeById = (id: string) => {
116 | return new Promise(async (resolve, reject) => {
117 | if (!id) {
118 | return resolve('')
119 | }
120 | const cfm = confirm('确定要删除么?')
121 | if (!cfm) {
122 | return resolve('')
123 | }
124 | await cloud.value
125 | .database()
126 | .collection(collection.value.name)
127 | .doc(id)
128 | .remove()
129 | await query()
130 | resolve('')
131 | // `cloud.value.database().collection('${collection.value.name}').${statement.value}`
132 | })
133 | }
134 |
135 | watchEffect(() => {
136 | if (history.value.length === 0) {
137 | history.value =
138 | JSON.parse(localStorage.getItem('laf_curd_query_history')) || []
139 | } else {
140 | localStorage.setItem(
141 | 'laf_curd_query_history',
142 | JSON.stringify(history.value)
143 | )
144 | }
145 | })
146 |
147 | return {
148 | appid,
149 | collection,
150 | statement,
151 | query,
152 | cloud,
153 | updateCloud,
154 | response,
155 | updateResponse,
156 | clear,
157 | updateById,
158 | removeById,
159 | history
160 | }
161 | })
162 |
--------------------------------------------------------------------------------
/stores/tab.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-07-12 20:08:25
5 | * @LastEditTime: 2023-07-15 19:36:23
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\tab.ts
9 | */
10 | import { defineStore } from 'pinia'
11 | interface Route {
12 | name: string
13 | params?: string[]
14 | }
15 | export const useTabStore = defineStore('useTabStore', () => {
16 | const list: Ref = ref([])
17 | const append = (route: Route) => {
18 | const exits = list.value.find(
19 | (item) =>
20 | item.name === route.name &&
21 | JSON.stringify(item.params) === JSON.stringify(route.params)
22 | )
23 | if (!exits) {
24 | list.value.push(route)
25 | }
26 | // const index = list.value.findIndex((item) => item === route)
27 | // console.log(index, route)
28 | // if (route && index === -1) {
29 | // list.value.push(route)
30 | // }
31 | }
32 |
33 | const remove = (index: number, jumpLast: boolean) => {
34 | // 如果剩一个,还是index,则不关闭
35 | if (list.value.length === 1 && list.value[0].name === 'index') {
36 | return
37 | }
38 | list.value.splice(index, 1)
39 | if (jumpLast) {
40 | if (list.value.length === 0) {
41 | navigateTo({ name: 'index' })
42 | } else {
43 | navigateTo(list.value[list.value.length - 1])
44 | }
45 | }
46 | }
47 |
48 | return {
49 | list,
50 | append,
51 | remove
52 | }
53 | })
54 |
--------------------------------------------------------------------------------
/stores/user.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: NMTuan
3 | * @Email: NMTuan@qq.com
4 | * @Date: 2023-06-30 20:30:22
5 | * @LastEditTime: 2023-07-02 14:45:26
6 | * @LastEditors: NMTuan
7 | * @Description:
8 | * @FilePath: \laf_curd\stores\user.ts
9 | */
10 | import { defineStore } from 'pinia'
11 |
12 | export const useUserStore = defineStore('useUserStore', () => {
13 | const token = useCookie('laf_curd_token')
14 | const whiteList = ['/welcome'] // 不需要鉴权的url路径
15 | const clearToken = () => {
16 | token.value = ''
17 | }
18 | return {
19 | token,
20 | whiteList,
21 | clearToken
22 | }
23 | })
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/uno.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, presetAttributify, presetUno } from 'unocss'
2 | import transformerDirectives from '@unocss/transformer-directives'
3 | import transformerVariantGroup from '@unocss/transformer-variant-group'
4 | import presetIcons from '@unocss/preset-icons'
5 |
6 | export default defineConfig({
7 | presets: [
8 | presetUno(), // default
9 | presetAttributify(), // attr mode: text="sm white"
10 | presetIcons() // icon: i-ri-home-fill
11 | ],
12 | transformers: [
13 | transformerDirectives(), // @apply or --at-apply
14 | transformerVariantGroup() // hover:(x x)
15 | ]
16 | })
17 |
--------------------------------------------------------------------------------