mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2025-09-23 12:06:36 +08:00
Merge pull request #227 from fast-crud/for-pr
feat(projects): integration fast-crud
This commit is contained in:
commit
032dbc6815
295
mock/api/crud/base.ts
Normal file
295
mock/api/crud/base.ts
Normal file
@ -0,0 +1,295 @@
|
||||
export type ListItem = {
|
||||
id?: number;
|
||||
children?: ListItem[];
|
||||
[key: string]: any;
|
||||
};
|
||||
export type BaseMockOptions = { name: string; copyTimes?: number; list: ListItem[]; idGenerator: number };
|
||||
type CopyListParams = { originList: ListItem[]; newList: ListItem[]; options: BaseMockOptions; parentId?: number };
|
||||
|
||||
function copyList(props: CopyListParams) {
|
||||
const { originList, newList, options, parentId } = props;
|
||||
for (const item of originList) {
|
||||
const newItem: ListItem = { ...item, parentId };
|
||||
newItem.id = options.idGenerator;
|
||||
options.idGenerator += 1;
|
||||
newList.push(newItem);
|
||||
if (item.children) {
|
||||
newItem.children = [];
|
||||
copyList({
|
||||
originList: item.children,
|
||||
newList: newItem.children,
|
||||
options,
|
||||
parentId: newItem.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delById(req: Service.MockOption, list: any[]) {
|
||||
for (let i = 0; i < list.length; i += 1) {
|
||||
const item = list[i];
|
||||
if (item.id === parseInt(req.query.id, 10)) {
|
||||
list.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
delById(req, item.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findById(id: number, list: ListItem[]): any {
|
||||
for (const item of list) {
|
||||
if (item.id === id) {
|
||||
return item;
|
||||
}
|
||||
if (item.children && item.children.length > 0) {
|
||||
const sub = findById(id, item.children);
|
||||
if (sub !== null && sub !== undefined) {
|
||||
return sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchWithArrayCondition(value: any[], item: ListItem, key: string) {
|
||||
if (value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
let matched = false;
|
||||
for (const i of value) {
|
||||
if (item[key] instanceof Array) {
|
||||
for (const j of item[key]) {
|
||||
if (i === j) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
break;
|
||||
}
|
||||
} else if (item[key] === i || (typeof item[key] === 'string' && item[key].indexOf(`${i}`) >= 0)) {
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
if (matched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
function matchWithObjectCondition(value: any, item: ListItem, key: string) {
|
||||
let matched = true;
|
||||
for (const key2 of Object.keys(value)) {
|
||||
const v = value[key2];
|
||||
if (v && item[key] && v !== item[key][key2]) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
function searchFromList(list: ListItem[], query: any) {
|
||||
const filter = (item: ListItem) => {
|
||||
let allFound = true; // 是否所有条件都符合
|
||||
for (const key of Object.keys(query)) {
|
||||
const value = query[key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
// no nothing
|
||||
} else if (value instanceof Array) {
|
||||
// 如果条件中的value是数组的话,只要查到一个就行
|
||||
const matched = matchWithArrayCondition(value, item, key);
|
||||
if (!matched) {
|
||||
allFound = false;
|
||||
}
|
||||
} else if (value instanceof Object) {
|
||||
// 如果条件中的value是对象的话,需要每个key都匹配
|
||||
const matched = matchWithObjectCondition(value, item, key);
|
||||
if (!matched) {
|
||||
allFound = false;
|
||||
}
|
||||
} else if (item[key] !== value) {
|
||||
allFound = false;
|
||||
}
|
||||
}
|
||||
return allFound;
|
||||
};
|
||||
return list.filter(filter);
|
||||
}
|
||||
|
||||
export default {
|
||||
buildMock(options: BaseMockOptions) {
|
||||
const name = options.name;
|
||||
if (!options.copyTimes) {
|
||||
options.copyTimes = 29;
|
||||
}
|
||||
const list: any[] = [];
|
||||
for (let i = 0; i < options.copyTimes; i += 1) {
|
||||
copyList({
|
||||
originList: options.list,
|
||||
newList: list,
|
||||
options
|
||||
});
|
||||
}
|
||||
options.list = list;
|
||||
return [
|
||||
{
|
||||
path: `/mock/${name}/page`,
|
||||
method: 'post',
|
||||
handle(req: Service.MockOption) {
|
||||
let data = [...list];
|
||||
let limit = 20;
|
||||
let offset = 0;
|
||||
for (const item of list) {
|
||||
if (item.children && item.children.length === 0) {
|
||||
item.hasChildren = false;
|
||||
item.lazy = false;
|
||||
}
|
||||
}
|
||||
let orderAsc: any;
|
||||
let orderProp: any;
|
||||
if (req && req.body) {
|
||||
const { page, query } = req.body;
|
||||
if (page.limit) {
|
||||
limit = parseInt(page.limit, 10);
|
||||
}
|
||||
if (page.offset) {
|
||||
offset = parseInt(page.offset, 10);
|
||||
}
|
||||
if (Object.keys(query).length > 0) {
|
||||
data = searchFromList(list, query);
|
||||
}
|
||||
}
|
||||
|
||||
const start = offset;
|
||||
let end = offset + limit;
|
||||
if (data.length < end) {
|
||||
end = data.length;
|
||||
}
|
||||
|
||||
if (orderProp) {
|
||||
// 排序
|
||||
data.sort((a, b) => {
|
||||
let ret = 0;
|
||||
if (a[orderProp] > b[orderProp]) {
|
||||
ret = 1;
|
||||
} else if (a[orderProp] < b[orderProp]) {
|
||||
ret = -1;
|
||||
}
|
||||
return orderAsc ? ret : -ret;
|
||||
});
|
||||
}
|
||||
|
||||
const records = data.slice(start, end);
|
||||
const lastOffset = data.length - (data.length % limit);
|
||||
if (offset > lastOffset) {
|
||||
offset = lastOffset;
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
records,
|
||||
total: data.length,
|
||||
limit,
|
||||
offset
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/get`,
|
||||
method: 'get',
|
||||
handle(req: Service.MockOption) {
|
||||
let id = req.query.id;
|
||||
id = parseInt(id, 10);
|
||||
let current = null;
|
||||
for (const item of list) {
|
||||
if (item.id === id) {
|
||||
current = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: current
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/add`,
|
||||
method: 'post',
|
||||
handle(req: Service.MockOption) {
|
||||
req.body.id = options.idGenerator;
|
||||
options.idGenerator += 1;
|
||||
list.unshift(req.body);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: req.body.id
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/update`,
|
||||
method: 'post',
|
||||
handle(req: Service.MockOption) {
|
||||
const item = findById(req.body.id, list);
|
||||
if (item) {
|
||||
Object.assign(item, req.body);
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/delete`,
|
||||
method: 'post',
|
||||
handle(req: Service.MockOption) {
|
||||
delById(req, list);
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/batchDelete`,
|
||||
method: 'post',
|
||||
handle(req: Service.MockOption) {
|
||||
const ids = req.body.ids;
|
||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||
const item = list[i];
|
||||
if (ids.indexOf(item.id) >= 0) {
|
||||
list.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: null
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
path: `/mock/${name}/all`,
|
||||
method: 'post',
|
||||
handle() {
|
||||
return {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: list
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
};
|
5
mock/api/crud/index.ts
Normal file
5
mock/api/crud/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import demo from './modules/demo';
|
||||
import headerGroup from './modules/header-group';
|
||||
|
||||
const crudApis = [...demo, ...headerGroup];
|
||||
export default crudApis;
|
56
mock/api/crud/modules/demo.ts
Normal file
56
mock/api/crud/modules/demo.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import type { MethodType, MockMethod } from 'vite-plugin-mock';
|
||||
import type { BaseMockOptions } from '../base';
|
||||
import mockBase from '../base';
|
||||
import MockOption = Service.MockOption;
|
||||
|
||||
const options: BaseMockOptions = {
|
||||
name: 'crud/demo',
|
||||
idGenerator: 0,
|
||||
list: [
|
||||
{
|
||||
select: '1',
|
||||
text: '文本测试',
|
||||
copyable: '文本可复制',
|
||||
avatar: 'http://greper.handsfree.work/extends/avatar.jpg',
|
||||
richtext: '富文本',
|
||||
datetime: '2023-01-30 11:11:11'
|
||||
},
|
||||
{
|
||||
select: '2'
|
||||
},
|
||||
{
|
||||
select: '0'
|
||||
}
|
||||
]
|
||||
};
|
||||
const mockedApis = mockBase.buildMock(options);
|
||||
|
||||
const apis: MockMethod[] = [
|
||||
{
|
||||
url: `/mock/${options.name}/dict`,
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
code: 200,
|
||||
message: '',
|
||||
data: [
|
||||
{ value: '0', label: '关', color: 'warning' },
|
||||
{ value: '1', label: '开', color: 'success' },
|
||||
{ value: '2', label: '停' }
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const mockedApi of mockedApis) {
|
||||
apis.push({
|
||||
url: mockedApi.path,
|
||||
method: mockedApi.method as MethodType,
|
||||
response: (request: MockOption) => {
|
||||
return mockedApi.handle(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default apis;
|
46
mock/api/crud/modules/header-group.ts
Normal file
46
mock/api/crud/modules/header-group.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import type { MethodType, MockMethod } from 'vite-plugin-mock';
|
||||
import type { BaseMockOptions } from '../base';
|
||||
import mockBase from '../base';
|
||||
import MockOption = Service.MockOption;
|
||||
|
||||
const options: BaseMockOptions = {
|
||||
name: 'crud/header-group',
|
||||
idGenerator: 0,
|
||||
list: [
|
||||
{
|
||||
name: '张三',
|
||||
age: 18,
|
||||
province: '广东省',
|
||||
city: '深圳市',
|
||||
county: '南山区',
|
||||
street: '粤海街道'
|
||||
},
|
||||
{
|
||||
name: '李四',
|
||||
age: 26,
|
||||
province: '浙江省',
|
||||
city: '杭州市',
|
||||
county: '西湖区',
|
||||
street: '西湖街道'
|
||||
},
|
||||
{
|
||||
name: '王五',
|
||||
age: 24
|
||||
}
|
||||
]
|
||||
};
|
||||
const mockedApis = mockBase.buildMock(options);
|
||||
|
||||
const apis: MockMethod[] = [];
|
||||
|
||||
for (const mockedApi of mockedApis) {
|
||||
apis.push({
|
||||
url: mockedApi.path,
|
||||
method: mockedApi.method as MethodType,
|
||||
response: (request: MockOption) => {
|
||||
return mockedApi.handle(request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default apis;
|
@ -1,5 +1,6 @@
|
||||
import auth from './auth';
|
||||
import route from './route';
|
||||
import management from './management';
|
||||
import crud from './crud';
|
||||
|
||||
export default [...auth, ...route, ...management];
|
||||
export default [...auth, ...route, ...management, ...crud];
|
||||
|
@ -56,6 +56,11 @@
|
||||
"prepare": "soy init-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"@fast-crud/fast-crud": "^1.13.6",
|
||||
"@fast-crud/fast-extends": "^1.13.6",
|
||||
"@fast-crud/ui-naive": "^1.13.6",
|
||||
"@fast-crud/ui-interface": "^1.13.6",
|
||||
"@antv/data-set": "^0.11.8",
|
||||
"@antv/g2": "^4.2.10",
|
||||
"@better-scroll/core": "^2.5.1",
|
||||
|
3005
pnpm-lock.yaml
3005
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,9 @@
|
||||
class="h-full"
|
||||
>
|
||||
<naive-provider>
|
||||
<router-view />
|
||||
<fs-ui-context>
|
||||
<router-view />
|
||||
</fs-ui-context>
|
||||
</naive-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
@ -3,7 +3,7 @@ import App from './App.vue';
|
||||
import AppLoading from './components/common/app-loading.vue';
|
||||
import { setupDirectives } from './directives';
|
||||
import { setupRouter } from './router';
|
||||
import { setupAssets } from './plugins';
|
||||
import { setupAssets, setupFastCrud } from './plugins';
|
||||
import { setupStore } from './store';
|
||||
import { setupI18n } from './locales';
|
||||
|
||||
@ -29,6 +29,8 @@ async function setupApp() {
|
||||
|
||||
setupI18n(app);
|
||||
|
||||
setupFastCrud(app);
|
||||
|
||||
// mount app
|
||||
app.mount('#app');
|
||||
}
|
||||
|
11
src/plugins/fast-crud/common.scss
Normal file
11
src/plugins/fast-crud/common.scss
Normal file
@ -0,0 +1,11 @@
|
||||
html:root {
|
||||
--baseColor: #fff;
|
||||
}
|
||||
/* 深色模式 */
|
||||
html.dark:root {
|
||||
--baseColor: #000;
|
||||
}
|
||||
|
||||
.fs-container {
|
||||
background-color: var(--baseColor);
|
||||
}
|
171
src/plugins/fast-crud/index.tsx
Normal file
171
src/plugins/fast-crud/index.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import type { App } from 'vue';
|
||||
import type { FsSetupOptions, PageQuery } from '@fast-crud/fast-crud';
|
||||
// eslint-disable-next-line import/order
|
||||
import { FastCrud } from '@fast-crud/fast-crud';
|
||||
import '@fast-crud/fast-crud/dist/style.css';
|
||||
import './common.scss';
|
||||
|
||||
import type { FsUploaderOptions } from '@fast-crud/fast-extends';
|
||||
import {
|
||||
FsExtendsCopyable,
|
||||
FsExtendsEditor,
|
||||
FsExtendsJson,
|
||||
FsExtendsTime,
|
||||
FsExtendsUploader
|
||||
} from '@fast-crud/fast-extends';
|
||||
import '@fast-crud/fast-extends/dist/style.css';
|
||||
import UiNaive from '@fast-crud/ui-naive';
|
||||
import axios from 'axios';
|
||||
import type { VueI18n } from 'vue-i18n';
|
||||
import { mockRequest, request } from '@/service/request';
|
||||
import { setupNaive } from '@/plugins/fast-crud/naive';
|
||||
|
||||
/**
|
||||
* fast-crud的安装方法
|
||||
* 注意:在App.vue中,需要用fs-ui-context组件包裹RouterView,让fs-crud拥有message、notification、dialog的能力
|
||||
* @param app
|
||||
* @param options
|
||||
*/
|
||||
export type FsSetupOpts = {
|
||||
i18n?: VueI18n;
|
||||
};
|
||||
function install(app: App, options: FsSetupOpts = {}) {
|
||||
// 安装naive ui 常用组件
|
||||
setupNaive(app);
|
||||
app.use(UiNaive);
|
||||
app.use(FastCrud, {
|
||||
i18n: options.i18n,
|
||||
async dictRequest(context: { url: string }) {
|
||||
const url = context.url;
|
||||
let res: Service.SuccessResult | Service.FailedResult;
|
||||
if (url && url.startsWith('/mock')) {
|
||||
// 如果是crud开头的dict请求视为mock
|
||||
res = await mockRequest.get(url.replace('/mock', ''));
|
||||
} else {
|
||||
res = await request.get(url);
|
||||
}
|
||||
res = res || {};
|
||||
return res.data || [];
|
||||
},
|
||||
/**
|
||||
* useCrud时会被执行
|
||||
*/
|
||||
commonOptions() {
|
||||
return {
|
||||
table: {
|
||||
size: 'small',
|
||||
pagination: false
|
||||
},
|
||||
search: {
|
||||
options: {
|
||||
size: 'medium'
|
||||
}
|
||||
},
|
||||
rowHandle: {
|
||||
buttons: {
|
||||
view: { text: null, icon: 'EyeOutlined', size: 'small' },
|
||||
edit: { text: null, icon: 'EditOutlined', size: 'small' },
|
||||
remove: { type: 'error', text: null, icon: 'DeleteOutlined', size: 'small' }
|
||||
},
|
||||
dropdown: {
|
||||
more: { size: 'small' }
|
||||
}
|
||||
},
|
||||
request: {
|
||||
// 查询参数转换
|
||||
transformQuery: (query: PageQuery) => {
|
||||
const { page, form, sort } = query;
|
||||
const limit = page.pageSize;
|
||||
const currentPage = page.currentPage ?? 1;
|
||||
const offset = limit * (currentPage - 1);
|
||||
|
||||
return {
|
||||
page: {
|
||||
limit,
|
||||
offset
|
||||
},
|
||||
query: form,
|
||||
sort: sort || {}
|
||||
};
|
||||
},
|
||||
// page请求结果转换
|
||||
transformRes: originPageRes => {
|
||||
const { res } = originPageRes;
|
||||
const pageSize = res.limit;
|
||||
let currentPage = res.offset / pageSize;
|
||||
if (res.offset % pageSize === 0) {
|
||||
currentPage += 1;
|
||||
}
|
||||
return { currentPage, pageSize, ...res };
|
||||
}
|
||||
},
|
||||
form: {
|
||||
display: 'flex', // 表单布局
|
||||
labelWidth: '120px' // 表单label宽度
|
||||
}
|
||||
};
|
||||
// 从 useCrud({permission}) 里获取permission参数,去设置各个按钮的权限
|
||||
// const crudPermission = useCrudPermission(context);
|
||||
// return crudPermission.merge(opts);
|
||||
}
|
||||
} as FsSetupOptions);
|
||||
|
||||
// fast-extends里面的扩展组件均为异步组件,只有在使用时才会被加载,并不会影响首页加载速度
|
||||
// 安装editor
|
||||
app.use(FsExtendsEditor, {
|
||||
// 编辑器的公共配置
|
||||
wangEditor: {}
|
||||
});
|
||||
app.use(FsExtendsJson);
|
||||
app.use(FsExtendsCopyable);
|
||||
// 安装uploader 公共参数
|
||||
const uploaderOptions: FsUploaderOptions = {
|
||||
defaultType: 'form',
|
||||
form: {
|
||||
action: 'http://www.docmirror.cn:7070/api/upload/form/upload',
|
||||
name: 'file',
|
||||
withCredentials: false,
|
||||
uploadRequest: async props => {
|
||||
const { action, file, onProgress } = props;
|
||||
const data = new FormData();
|
||||
data.append('file', file);
|
||||
const res = await axios.post(action, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 60000,
|
||||
onUploadProgress(progress) {
|
||||
onProgress({ percent: Math.round((progress.loaded / progress.total!) * 100) });
|
||||
}
|
||||
});
|
||||
// 上传完成后的结果,一般返回个url 或者key,具体看你的后台返回啥
|
||||
return res.data.data;
|
||||
},
|
||||
async successHandle(ret: string) {
|
||||
// 上传完成后的结果处理, 此处应转换格式为{url:xxx,key:xxx}
|
||||
return {
|
||||
url: `http://www.docmirror.cn:7070${ret}`,
|
||||
key: ret.replace('/api/upload/form/download?key=', '')
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
app.use(FsExtendsUploader, uploaderOptions);
|
||||
|
||||
// 安装editor
|
||||
app.use(FsExtendsEditor, {
|
||||
// 编辑器的公共配置
|
||||
wangEditor: {}
|
||||
});
|
||||
app.use(FsExtendsJson);
|
||||
app.use(FsExtendsTime);
|
||||
app.use(FsExtendsCopyable);
|
||||
}
|
||||
|
||||
export default {
|
||||
install
|
||||
};
|
||||
|
||||
export function setupFastCrud(app: App<Element>, options: FsSetupOpts = {}) {
|
||||
install(app, options);
|
||||
}
|
59
src/plugins/fast-crud/naive.ts
Normal file
59
src/plugins/fast-crud/naive.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { App } from 'vue';
|
||||
import * as NaiveUI from 'naive-ui';
|
||||
|
||||
const naive = NaiveUI.create({
|
||||
components: [
|
||||
NaiveUI.NInput,
|
||||
NaiveUI.NButton,
|
||||
NaiveUI.NForm,
|
||||
NaiveUI.NFormItem,
|
||||
NaiveUI.NCheckboxGroup,
|
||||
NaiveUI.NCheckbox,
|
||||
NaiveUI.NIcon,
|
||||
NaiveUI.NDropdown,
|
||||
NaiveUI.NTooltip,
|
||||
NaiveUI.NTabs,
|
||||
NaiveUI.NTabPane,
|
||||
NaiveUI.NCard,
|
||||
NaiveUI.NRow,
|
||||
NaiveUI.NCol,
|
||||
NaiveUI.NDrawer,
|
||||
NaiveUI.NDrawerContent,
|
||||
NaiveUI.NDivider,
|
||||
NaiveUI.NSwitch,
|
||||
NaiveUI.NBadge,
|
||||
NaiveUI.NAlert,
|
||||
NaiveUI.NTag,
|
||||
NaiveUI.NProgress,
|
||||
NaiveUI.NDatePicker,
|
||||
NaiveUI.NGrid,
|
||||
NaiveUI.NGridItem,
|
||||
NaiveUI.NDataTable,
|
||||
NaiveUI.NPagination,
|
||||
NaiveUI.NSelect,
|
||||
NaiveUI.NRadioGroup,
|
||||
NaiveUI.NRadio,
|
||||
NaiveUI.NInputGroup,
|
||||
NaiveUI.NTable,
|
||||
NaiveUI.NInputNumber,
|
||||
NaiveUI.NLoadingBarProvider,
|
||||
NaiveUI.NModal,
|
||||
NaiveUI.NUpload,
|
||||
NaiveUI.NTree,
|
||||
NaiveUI.NSpin,
|
||||
NaiveUI.NTimePicker,
|
||||
|
||||
// add by fs
|
||||
NaiveUI.NCascader,
|
||||
NaiveUI.NRadioButton,
|
||||
NaiveUI.NTreeSelect,
|
||||
NaiveUI.NImageGroup,
|
||||
NaiveUI.NImage,
|
||||
NaiveUI.NCollapse,
|
||||
NaiveUI.NCollapseItem
|
||||
]
|
||||
});
|
||||
|
||||
export function setupNaive(app: App<Element>) {
|
||||
app.use(naive);
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { setupFastCrud } from '@/plugins/fast-crud';
|
||||
import setupAssets from './assets';
|
||||
|
||||
export { setupFastCrud };
|
||||
export { setupAssets };
|
||||
|
45
src/router/modules/crud.ts
Normal file
45
src/router/modules/crud.ts
Normal file
@ -0,0 +1,45 @@
|
||||
const component: any = {
|
||||
name: 'crud',
|
||||
path: '/crud',
|
||||
component: 'basic',
|
||||
meta: {
|
||||
title: 'CRUD示例',
|
||||
requiresAuth: true,
|
||||
icon: 'mdi:table-large',
|
||||
order: 4
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'crud_demo',
|
||||
path: '/crud/demo',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '基本示例',
|
||||
requiresAuth: true,
|
||||
icon: 'mdi:button-cursor'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'crud_header_group',
|
||||
path: '/crud/header_group',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: '多级表头',
|
||||
requiresAuth: true,
|
||||
icon: 'mdi:button-cursor'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'crud_doc',
|
||||
path: '/crud/doc',
|
||||
component: 'self',
|
||||
meta: {
|
||||
title: 'FastCrud文档',
|
||||
requiresAuth: true,
|
||||
icon: 'logos:vue'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default component;
|
10
src/typings/page-route.d.ts
vendored
10
src/typings/page-route.d.ts
vendored
@ -30,6 +30,12 @@ declare namespace PageRoute {
|
||||
| 'component_button'
|
||||
| 'component_card'
|
||||
| 'component_table'
|
||||
| 'crud'
|
||||
| 'crud_demo'
|
||||
| 'crud_doc'
|
||||
| 'crud_header'
|
||||
| 'crud_header_group'
|
||||
| 'crud_source'
|
||||
| 'dashboard'
|
||||
| 'dashboard_analysis'
|
||||
| 'dashboard_workbench'
|
||||
@ -89,6 +95,10 @@ declare namespace PageRoute {
|
||||
| 'component_button'
|
||||
| 'component_card'
|
||||
| 'component_table'
|
||||
| 'crud_demo'
|
||||
| 'crud_doc'
|
||||
| 'crud_header_group'
|
||||
| 'crud_source'
|
||||
| 'dashboard_analysis'
|
||||
| 'dashboard_workbench'
|
||||
| 'document_naive'
|
||||
|
43
src/views/crud/demo/api.ts
Normal file
43
src/views/crud/demo/api.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { UserPageQuery } from '@fast-crud/fast-crud';
|
||||
import { mockRequest } from '@/service/request';
|
||||
|
||||
const request = mockRequest;
|
||||
const apiPrefix = '/crud/demo';
|
||||
|
||||
export type DemoRecord = {
|
||||
id: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
function resHandle(res: any) {
|
||||
return res.data;
|
||||
}
|
||||
export async function GetList(query: UserPageQuery) {
|
||||
const res = await request.post(`${apiPrefix}/page`, query);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function AddObj(obj: DemoRecord) {
|
||||
const res = await request.post(`${apiPrefix}/add`, obj);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function UpdateObj(obj: DemoRecord) {
|
||||
const res = await request.post(`${apiPrefix}/update`, obj);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function DelObj(id: number) {
|
||||
const res = await request.post(`${apiPrefix}/delete`, { id });
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function GetObj(id: number) {
|
||||
const res = await request.get(`${apiPrefix}/info`, { params: { id } });
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function BatchDelete(ids: number[]) {
|
||||
const res = await request.post(`${apiPrefix}/batchDelete`, { ids });
|
||||
return resHandle(res);
|
||||
}
|
114
src/views/crud/demo/crud.tsx
Normal file
114
src/views/crud/demo/crud.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import type { AddReq, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from '@fast-crud/fast-crud';
|
||||
import { dict } from '@fast-crud/fast-crud';
|
||||
import dayjs from 'dayjs';
|
||||
import * as api from './api';
|
||||
|
||||
export default function createCrudOptions(): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return api.GetList(query);
|
||||
};
|
||||
const editRequest = async (ctx: EditReq) => {
|
||||
const { form, row } = ctx;
|
||||
form.id = row.id;
|
||||
return api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async (ctx: DelReq) => {
|
||||
const { row } = ctx;
|
||||
return api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async (req: AddReq) => {
|
||||
const { form } = req;
|
||||
return api.AddObj(form);
|
||||
};
|
||||
return {
|
||||
crudOptions: {
|
||||
container: {
|
||||
is: 'fs-layout-card'
|
||||
},
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
columns: {
|
||||
id: {
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
type: 'number',
|
||||
column: {
|
||||
width: 50
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
datetime: {
|
||||
title: '时间',
|
||||
type: 'datetime',
|
||||
// naive 默认仅支持数字类型时间戳作为日期输入与输出
|
||||
// 字符串类型的时间需要转换格式
|
||||
valueBuilder(context) {
|
||||
const { value, row, key } = context;
|
||||
if (value) {
|
||||
// naive 默认仅支持时间戳作为日期输入与输出
|
||||
row[key] = dayjs(value).valueOf();
|
||||
}
|
||||
},
|
||||
valueResolve(context) {
|
||||
const { value, form, key } = context;
|
||||
if (value) {
|
||||
form[key] = dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
title: '状态',
|
||||
search: { show: true },
|
||||
type: 'dict-select',
|
||||
dict: dict({
|
||||
url: '/mock/crud/demo/dict'
|
||||
})
|
||||
},
|
||||
text: {
|
||||
title: '文本',
|
||||
type: 'text',
|
||||
search: { show: true }
|
||||
},
|
||||
copyable: {
|
||||
title: '可复制',
|
||||
type: ['text', 'copyable'],
|
||||
search: { show: true }
|
||||
},
|
||||
avatar: {
|
||||
title: '头像裁剪',
|
||||
type: 'cropper-uploader'
|
||||
},
|
||||
upload: {
|
||||
title: '文件上传',
|
||||
type: 'file-uploader'
|
||||
},
|
||||
richtext: {
|
||||
title: '富文本',
|
||||
type: 'editor-wang5',
|
||||
column: {
|
||||
// cell中不显示
|
||||
show: false
|
||||
},
|
||||
form: {
|
||||
col: {
|
||||
// 横跨两列
|
||||
span: 24
|
||||
},
|
||||
component: {
|
||||
style: {
|
||||
height: '300px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
28
src/views/crud/demo/index.vue
Normal file
28
src/views/crud/demo/index.vue
Normal file
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import createCrudOptions from './crud';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ComponentCrud',
|
||||
setup() {
|
||||
const { crudRef, crudBinding, crudExpose } = useFs({ createCrudOptions });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
13
src/views/crud/doc/index.vue
Normal file
13
src/views/crud/doc/index.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('http://fast-crud.docmirror.cn/');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
43
src/views/crud/header_group/api.ts
Normal file
43
src/views/crud/header_group/api.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { UserPageQuery } from '@fast-crud/fast-crud';
|
||||
import { mockRequest } from '@/service/request';
|
||||
|
||||
const request = mockRequest;
|
||||
const apiPrefix = '/crud/header-group';
|
||||
|
||||
export type HeaderGroupRecord = {
|
||||
id: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
function resHandle(res: any) {
|
||||
return res.data;
|
||||
}
|
||||
export async function GetList(query: UserPageQuery) {
|
||||
const res = await request.post(`${apiPrefix}/page`, query);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function AddObj(obj: HeaderGroupRecord) {
|
||||
const res = await request.post(`${apiPrefix}/add`, obj);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function UpdateObj(obj: HeaderGroupRecord) {
|
||||
const res = await request.post(`${apiPrefix}/update`, obj);
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function DelObj(id: number) {
|
||||
const res = await request.post(`${apiPrefix}/delete`, { id });
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function GetObj(id: number) {
|
||||
const res = await request.get(`${apiPrefix}/info`, { params: { id } });
|
||||
return resHandle(res);
|
||||
}
|
||||
|
||||
export async function BatchDelete(ids: number[]) {
|
||||
const res = await request.post(`${apiPrefix}/batchDelete`, { ids });
|
||||
return resHandle(res);
|
||||
}
|
96
src/views/crud/header_group/crud.tsx
Normal file
96
src/views/crud/header_group/crud.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import type { CreateCrudOptionsRet, UserPageQuery, UserPageRes } from '@fast-crud/fast-crud';
|
||||
import type { HeaderGroupRecord } from './api';
|
||||
import * as api from './api';
|
||||
|
||||
export default function createCrudOptions(): CreateCrudOptionsRet {
|
||||
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||
return api.GetList(query);
|
||||
};
|
||||
const editRequest = async (ctx: { form: HeaderGroupRecord; row: HeaderGroupRecord }) => {
|
||||
const { form, row } = ctx;
|
||||
form.id = row.id;
|
||||
return api.UpdateObj(form);
|
||||
};
|
||||
const delRequest = async (ctx: { row: HeaderGroupRecord }) => {
|
||||
const { row } = ctx;
|
||||
return api.DelObj(row.id);
|
||||
};
|
||||
|
||||
const addRequest = async (ctx: { form: HeaderGroupRecord }) => {
|
||||
const { form } = ctx;
|
||||
return api.AddObj(form);
|
||||
};
|
||||
return {
|
||||
crudOptions: {
|
||||
container: {
|
||||
// is: 'fs-layout-card'
|
||||
},
|
||||
request: {
|
||||
pageRequest,
|
||||
addRequest,
|
||||
editRequest,
|
||||
delRequest
|
||||
},
|
||||
form: {
|
||||
layout: 'flex',
|
||||
labelWidth: '100px' // 表单label宽度
|
||||
},
|
||||
table: { size: 'small' },
|
||||
columns: {
|
||||
id: {
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
type: 'number',
|
||||
column: {
|
||||
width: 50
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
user: {
|
||||
title: '用户信息',
|
||||
children: {
|
||||
name: {
|
||||
title: '姓名',
|
||||
type: 'text'
|
||||
},
|
||||
age: {
|
||||
title: '年龄',
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
},
|
||||
address: {
|
||||
title: '地址',
|
||||
children: {
|
||||
area: {
|
||||
title: '地区',
|
||||
children: {
|
||||
province: {
|
||||
title: '省',
|
||||
type: 'text',
|
||||
search: { show: true }
|
||||
},
|
||||
city: {
|
||||
title: '市',
|
||||
search: { show: true },
|
||||
type: 'text'
|
||||
},
|
||||
county: {
|
||||
title: '区',
|
||||
search: { show: true },
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
},
|
||||
street: {
|
||||
title: '街道',
|
||||
type: 'text'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
32
src/views/crud/header_group/index.vue
Normal file
32
src/views/crud/header_group/index.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="h-full fs-page-header-group">
|
||||
<fs-crud ref="crudRef" v-bind="crudBinding" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
import { useFs } from '@fast-crud/fast-crud';
|
||||
import createCrudOptions from './crud';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CrudHeaderGroup',
|
||||
setup() {
|
||||
const { crudRef, crudBinding, crudExpose } = useFs({ createCrudOptions });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
onMounted(() => {
|
||||
crudExpose.doRefresh();
|
||||
});
|
||||
|
||||
return {
|
||||
crudBinding,
|
||||
crudRef
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.fs-page-header-group {
|
||||
}
|
||||
</style>
|
13
src/views/crud/source/index.vue
Normal file
13
src/views/crud/source/index.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<iframe class="wh-full" :src="src"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const src = ref('https://github.com/fast-crud/fast-crud');
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -16,6 +16,10 @@ export const views: Record<
|
||||
component_button: () => import('./component/button/index.vue'),
|
||||
component_card: () => import('./component/card/index.vue'),
|
||||
component_table: () => import('./component/table/index.vue'),
|
||||
crud_demo: () => import('./crud/demo/index.vue'),
|
||||
crud_doc: () => import('./crud/doc/index.vue'),
|
||||
crud_header_group: () => import('./crud/header_group/index.vue'),
|
||||
crud_source: () => import('./crud/source/index.vue'),
|
||||
dashboard_analysis: () => import('./dashboard/analysis/index.vue'),
|
||||
dashboard_workbench: () => import('./dashboard/workbench/index.vue'),
|
||||
document_naive: () => import('./document/naive/index.vue'),
|
||||
|
Loading…
Reference in New Issue
Block a user