├── .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 | ![img](README/image-20230716135559277.png) 24 | 25 | 登录后是下面这个样子,左侧是主菜单,右侧是内容区域。值得说一句的是这次用了多tabs的模式。这也算是管理平台的标配了。 26 | 27 | ![img](README/image-20230716135635973.png) 28 | 29 | 下面这个是自定义查询界面,复杂的查询语句都可以在这里尝试。历史语句都保存在浏览器缓存中,放心使用。 30 | 31 | 这里支持 laf 的查询语句,支持 `_` 关键字。 32 | 33 | ![image-20230716135655742](README/image-20230716135655742.png) 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 | ![image-20230716135712479](README/image-20230716135712479.png) 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 | 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 | 15 | 28 | -------------------------------------------------------------------------------- /components/applications/list/func.vue: -------------------------------------------------------------------------------- 1 | 10 | 22 | 31 | 32 | 46 | -------------------------------------------------------------------------------- /components/applications/list/item.vue: -------------------------------------------------------------------------------- 1 | 10 | 18 | 26 | -------------------------------------------------------------------------------- /components/database/detail.vue: -------------------------------------------------------------------------------- 1 | 10 | 25 | 33 | 47 | -------------------------------------------------------------------------------- /components/database/edit.vue: -------------------------------------------------------------------------------- 1 | 10 | 29 | 81 | 96 | -------------------------------------------------------------------------------- /components/database/fields.vue: -------------------------------------------------------------------------------- 1 | 10 | 84 | 185 | -------------------------------------------------------------------------------- /components/database/history.vue: -------------------------------------------------------------------------------- 1 | 11 | 25 | -------------------------------------------------------------------------------- /components/database/list.vue: -------------------------------------------------------------------------------- 1 | 10 | 19 | 31 | -------------------------------------------------------------------------------- /components/database/list/item.vue: -------------------------------------------------------------------------------- 1 | 9 | 21 | 43 | -------------------------------------------------------------------------------- /components/database/response.vue: -------------------------------------------------------------------------------- 1 | 10 | 28 | 32 | 40 | -------------------------------------------------------------------------------- /components/database/run.vue: -------------------------------------------------------------------------------- 1 | 10 | 28 | 87 | -------------------------------------------------------------------------------- /components/database/table.vue: -------------------------------------------------------------------------------- 1 | 30 | 67 | 110 | -------------------------------------------------------------------------------- /components/layout/drawer.vue: -------------------------------------------------------------------------------- 1 | 10 | 17 | 39 | 62 | -------------------------------------------------------------------------------- /components/layout/footer.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /components/layout/header.vue: -------------------------------------------------------------------------------- 1 | 10 | 24 | -------------------------------------------------------------------------------- /components/layout/loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 15 | 23 | -------------------------------------------------------------------------------- /components/layout/panel.vue: -------------------------------------------------------------------------------- 1 | 10 | 26 | 39 | 51 | -------------------------------------------------------------------------------- /components/layout/twoColumn.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /components/manager/create.vue: -------------------------------------------------------------------------------- 1 | 10 | 18 | 55 | 70 | -------------------------------------------------------------------------------- /components/manager/detail.vue: -------------------------------------------------------------------------------- 1 | 10 | 21 | 37 | 46 | -------------------------------------------------------------------------------- /components/manager/edit.vue: -------------------------------------------------------------------------------- 1 | 10 | 18 | 78 | 85 | -------------------------------------------------------------------------------- /components/manager/header.vue: -------------------------------------------------------------------------------- 1 | 10 | 52 | 110 | 121 | -------------------------------------------------------------------------------- /components/manager/history.vue: -------------------------------------------------------------------------------- 1 | 10 | 23 | 43 | -------------------------------------------------------------------------------- /components/manager/runHistory.vue: -------------------------------------------------------------------------------- 1 | 10 | 26 | 46 | -------------------------------------------------------------------------------- /components/manager/side.vue: -------------------------------------------------------------------------------- 1 | 10 | 36 | 122 | 157 | -------------------------------------------------------------------------------- /components/manager/tab.vue: -------------------------------------------------------------------------------- 1 | 10 | 27 | 46 | 118 | -------------------------------------------------------------------------------- /components/manager/tmp.vue: -------------------------------------------------------------------------------- 1 | 8 | 16 | -------------------------------------------------------------------------------- /components/scrollBox.vue: -------------------------------------------------------------------------------- 1 | 10 | 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 | 17 | -------------------------------------------------------------------------------- /layouts/manager.vue: -------------------------------------------------------------------------------- 1 | 10 | 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 | 39 | -------------------------------------------------------------------------------- /pages/index/[...key].vue: -------------------------------------------------------------------------------- 1 | 53 | 226 | 253 | -------------------------------------------------------------------------------- /pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 16 | -------------------------------------------------------------------------------- /pages/index/query.vue: -------------------------------------------------------------------------------- 1 | 10 | 76 | 261 | 302 | -------------------------------------------------------------------------------- /pages/old/app/[appid].vue: -------------------------------------------------------------------------------- 1 | 13 | 17 | -------------------------------------------------------------------------------- /pages/old/app/[appid]/database.vue: -------------------------------------------------------------------------------- 1 | 10 | 21 | 24 | -------------------------------------------------------------------------------- /pages/old/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 15 | -------------------------------------------------------------------------------- /pages/welcome.vue: -------------------------------------------------------------------------------- 1 | 10 | 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 | --------------------------------------------------------------------------------