mirror of
https://github.com/soybeanjs/soybean-admin.git
synced 2026-02-08 00:01:23 +08:00
Compare commits
9 Commits
232e1ac40d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a37949f2af | ||
|
|
54107acaac | ||
|
|
cbfb932f8e | ||
|
|
dacee143c0 | ||
|
|
2a0231da55 | ||
|
|
d73947a5ab | ||
|
|
0081b9c022 | ||
|
|
30e3cdc7b9 | ||
|
|
706586439d |
34
.github/workflows/opencode.yml
vendored
Normal file
34
.github/workflows/opencode.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: opencode
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
opencode:
|
||||
if: |
|
||||
contains(github.event.comment.body, ' /oc') ||
|
||||
startsWith(github.event.comment.body, '/oc') ||
|
||||
contains(github.event.comment.body, ' /opencode') ||
|
||||
startsWith(github.event.comment.body, '/opencode')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run opencode
|
||||
uses: anomalyco/opencode/github@latest
|
||||
env:
|
||||
GOOGLE_GENERATIVE_AI_API_KEY: ${{ secrets.GOOGLE_GENERATIVE_AI_API_KEY }}
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
with:
|
||||
model: google/gemini-3-pro-preview
|
||||
@@ -23,9 +23,6 @@
|
||||
> [!NOTE]
|
||||
> The `SoybeanAdmin` quick start series videos have been uploaded to [Bilibili](https://www.bilibili.com/video/BV1YKdRYXELC) Go online [click here](https://www.bilibili.com/video/BV1YKdRYXELC) Go check it out
|
||||
|
||||
> [!WARNING]
|
||||
> `SoybeanAdmin` is planning to develop a `V2` version, see [plan list](https://github.com/soybeanjs/soybean-admin/issues/767)
|
||||
|
||||
## Introduction
|
||||
|
||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) is a clean, elegant, beautiful and powerful admin template, based on the latest front-end technology stack, including Vue3, Vite7, TypeScript, Pinia and UnoCSS. It has built-in rich theme configuration and components, strict code specifications, and an automated file routing system. In addition, it also uses the online mock data solution based on ApiFox. `SoybeanAdmin` provides you with a one-stop admin solution, no additional configuration, and out of the box. It is also a best practice for learning cutting-edge technologies quickly.
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
> [!NOTE]
|
||||
> `SoybeanAdmin` 快速上手系列视频已在 [Bilibili](https://www.bilibili.com/video/BV1YKdRYXELC) 上线 [点击这里](https://www.bilibili.com/video/BV1YKdRYXELC) 前往查看
|
||||
|
||||
> [!WARNING]
|
||||
> `SoybeanAdmin` 正在计划开发 `V2` 版本,详情见[计划清单](https://github.com/soybeanjs/soybean-admin/issues/767)
|
||||
|
||||
## 简介
|
||||
|
||||
[`SoybeanAdmin`](https://github.com/soybeanjs/soybean-admin) 是一个清新优雅、高颜值且功能强大的后台管理模板,基于最新的前端技术栈,包括 Vue3, Vite7, TypeScript, Pinia 和 UnoCSS。它内置了丰富的主题配置和组件,代码规范严谨,实现了自动化的文件路由系统。此外,它还采用了基于 ApiFox 的在线Mock数据方案。`SoybeanAdmin` 为您提供了一站式的后台管理解决方案,无需额外配置,开箱即用。同样是一个快速学习前沿技术的最佳实践。
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { PluginOption } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx';
|
||||
import progress from 'vite-plugin-progress';
|
||||
import vueRootValidator from 'vite-plugin-vue-transition-root-validator';
|
||||
import { setupElegantRouter } from './router';
|
||||
import { setupUnocss } from './unocss';
|
||||
import { setupUnplugin } from './unplugin';
|
||||
@@ -17,7 +18,8 @@ export function setupVitePlugins(viteEnv: Env.ImportMeta, buildTime: string) {
|
||||
setupUnocss(viteEnv),
|
||||
...setupUnplugin(viteEnv),
|
||||
progress(),
|
||||
setupHtmlPlugin(buildTime)
|
||||
setupHtmlPlugin(buildTime),
|
||||
vueRootValidator()
|
||||
];
|
||||
|
||||
return plugins;
|
||||
|
||||
35
package.json
35
package.json
@@ -64,43 +64,44 @@
|
||||
"nprogress": "0.2.0",
|
||||
"pinia": "3.0.4",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"vue": "3.5.26",
|
||||
"vue-draggable-plus": "0.6.0",
|
||||
"vue-i18n": "11.2.7",
|
||||
"vue": "3.5.27",
|
||||
"vue-draggable-plus": "0.6.1",
|
||||
"vue-i18n": "11.2.8",
|
||||
"vue-router": "4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@elegant-router/vue": "0.3.8",
|
||||
"@iconify/json": "2.2.417",
|
||||
"@iconify/json": "2.2.430",
|
||||
"@sa/scripts": "workspace:*",
|
||||
"@sa/uno-preset": "workspace:*",
|
||||
"@soybeanjs/eslint-config": "1.7.5",
|
||||
"@types/node": "25.0.3",
|
||||
"@types/node": "25.0.9",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/eslint-config": "66.5.10",
|
||||
"@unocss/preset-icons": "66.5.10",
|
||||
"@unocss/preset-uno": "66.5.10",
|
||||
"@unocss/transformer-directives": "66.5.10",
|
||||
"@unocss/transformer-variant-group": "66.5.10",
|
||||
"@unocss/vite": "66.5.10",
|
||||
"@unocss/eslint-config": "66.6.0",
|
||||
"@unocss/preset-icons": "66.6.0",
|
||||
"@unocss/preset-uno": "66.6.0",
|
||||
"@unocss/transformer-directives": "66.6.0",
|
||||
"@unocss/transformer-variant-group": "66.6.0",
|
||||
"@unocss/vite": "66.6.0",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"@vitejs/plugin-vue-jsx": "5.1.3",
|
||||
"consola": "3.4.2",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"kolorist": "1.8.0",
|
||||
"sass": "1.97.1",
|
||||
"sass": "1.97.2",
|
||||
"simple-git-hooks": "2.13.1",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"unplugin-icons": "22.5.0",
|
||||
"unplugin-vue-components": "30.0.0",
|
||||
"vite": "7.3.0",
|
||||
"unplugin-icons": "23.0.1",
|
||||
"unplugin-vue-components": "31.0.0",
|
||||
"vite": "7.3.1",
|
||||
"vite-plugin-progress": "0.0.7",
|
||||
"vite-plugin-svg-icons": "2.0.1",
|
||||
"vite-plugin-vue-devtools": "8.0.5",
|
||||
"vite-plugin-vue-transition-root-validator": "^0.0.4",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.2.1"
|
||||
"vue-tsc": "3.2.2"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"commit-msg": "pnpm sa git-commit-verify",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@sa/utils": "workspace:*",
|
||||
"axios": "1.13.2",
|
||||
"axios-retry": "4.5.0",
|
||||
"qs": "6.14.0"
|
||||
"qs": "6.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/qs": "6.14.0"
|
||||
|
||||
@@ -23,6 +23,7 @@ export type TableColumnCheck = {
|
||||
title: TableColumnCheckTitle;
|
||||
checked: boolean;
|
||||
visible: boolean;
|
||||
fixed: 'left' | 'right' | 'unFixed';
|
||||
};
|
||||
|
||||
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
|
||||
@@ -78,12 +79,14 @@ export default function useTable<ResponseData, ApiData, Column, Pagination exten
|
||||
|
||||
function reloadColumns() {
|
||||
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||
const fixedMap = new Map(columnChecks.value.map(col => [col.key, col.fixed]));
|
||||
|
||||
const defaultChecks = getColumnChecks(columns());
|
||||
|
||||
columnChecks.value = defaultChecks.map(col => ({
|
||||
...col,
|
||||
checked: checkMap.get(col.key) ?? col.checked
|
||||
checked: checkMap.get(col.key) ?? col.checked,
|
||||
fixed: (fixedMap.get(col.key) !== 'unFixed' ? fixedMap.get(col.key) : undefined) ?? col.fixed
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@soybeanjs/changelog": "0.4.3",
|
||||
"bumpp": "10.3.2",
|
||||
"bumpp": "10.4.0",
|
||||
"c12": "3.3.3",
|
||||
"cac": "6.7.14",
|
||||
"consola": "3.4.2",
|
||||
"enquirer": "2.4.1",
|
||||
"execa": "9.6.1",
|
||||
"kolorist": "1.8.0",
|
||||
"npm-check-updates": "19.2.0",
|
||||
"npm-check-updates": "19.3.1",
|
||||
"picomatch": "4.0.3",
|
||||
"rimraf": "6.1.2"
|
||||
}
|
||||
|
||||
1657
pnpm-lock.yaml
generated
1657
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts" generic="T extends Record<string, unknown>, K = never">
|
||||
import { computed } from 'vue';
|
||||
import { VueDraggable } from 'vue-draggable-plus';
|
||||
import { $t } from '@/locales';
|
||||
|
||||
@@ -9,6 +10,53 @@ defineOptions({
|
||||
const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
required: true
|
||||
});
|
||||
|
||||
const tooltipRecord: Record<NaiveUI.TableColumnFixed, App.I18n.I18nKey> = {
|
||||
left: 'datatable.fixed.right',
|
||||
right: 'datatable.fixed.unFixed',
|
||||
unFixed: 'datatable.fixed.left'
|
||||
};
|
||||
|
||||
function handleFixed(column: NaiveUI.TableColumnCheck) {
|
||||
const fixedOptions: NaiveUI.TableColumnFixed[] = ['left', 'right', 'unFixed'];
|
||||
const index = fixedOptions.findIndex(item => item === column.fixed);
|
||||
const nextIndex = index === fixedOptions.length - 1 ? 0 : index + 1;
|
||||
column.fixed = fixedOptions[nextIndex];
|
||||
}
|
||||
|
||||
const visibleStats = computed(() => {
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
|
||||
columns.value.forEach(column => {
|
||||
if (!column.visible) return;
|
||||
|
||||
total += 1;
|
||||
if (column.checked) checked += 1;
|
||||
});
|
||||
|
||||
return { total, checked };
|
||||
});
|
||||
|
||||
const selectAllChecked = computed(() => {
|
||||
const { total, checked } = visibleStats.value;
|
||||
|
||||
return total > 0 && checked === total;
|
||||
});
|
||||
|
||||
const selectAllIndeterminate = computed(() => {
|
||||
const { total, checked } = visibleStats.value;
|
||||
|
||||
return checked > 0 && checked < total;
|
||||
});
|
||||
|
||||
function toggleSelectAll(checked: boolean) {
|
||||
columns.value.forEach(column => {
|
||||
if (!column.visible) return;
|
||||
|
||||
column.checked = checked;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -21,22 +69,48 @@ const columns = defineModel<NaiveUI.TableColumnCheck[]>('columns', {
|
||||
{{ $t('common.columnSetting') }}
|
||||
</NButton>
|
||||
</template>
|
||||
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable">
|
||||
<div
|
||||
v-for="item in columns"
|
||||
:key="item.key"
|
||||
class="h-36px flex-y-center rd-4px hover:(bg-primary bg-opacity-20)"
|
||||
:class="{ hidden: !item.visible }"
|
||||
>
|
||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||
<template v-if="typeof item.title === 'function'">
|
||||
<component :is="item.title" />
|
||||
</template>
|
||||
<template v-else>{{ item.title }}</template>
|
||||
<div>
|
||||
<div class="h-36px flex-y-center rd-4px pl-26px hover:(bg-primary bg-opacity-20)">
|
||||
<NCheckbox
|
||||
:checked="selectAllChecked"
|
||||
:indeterminate="selectAllIndeterminate"
|
||||
:disabled="visibleStats.total === 0"
|
||||
class="flex-1"
|
||||
@update:checked="toggleSelectAll"
|
||||
>
|
||||
{{ $t('common.selectAll') }}
|
||||
</NCheckbox>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<NDivider class="!my-4px" />
|
||||
<VueDraggable v-model="columns" :animation="150" filter=".none_draggable" class="max-h-[200px] overflow-y-auto">
|
||||
<div
|
||||
v-for="item in columns"
|
||||
:key="item.key"
|
||||
class="h-36px flex-y-center justify-between gap-6px"
|
||||
:class="{ hidden: !item.visible }"
|
||||
>
|
||||
<div class="h-full flex-y-center flex-1 rd-4px hover:(bg-primary bg-opacity-20)">
|
||||
<icon-mdi-drag class="mr-8px h-full cursor-move text-icon" />
|
||||
<NCheckbox v-model:checked="item.checked" class="none_draggable flex-1">
|
||||
<template v-if="typeof item.title === 'function'">
|
||||
<component :is="item.title" />
|
||||
</template>
|
||||
<template v-else>{{ item.title }}</template>
|
||||
</NCheckbox>
|
||||
</div>
|
||||
<ButtonIcon
|
||||
:disabled="!item.checked"
|
||||
:focusable="false"
|
||||
:tooltip-content="$t(tooltipRecord[item.fixed!])"
|
||||
@click="handleFixed(item)"
|
||||
>
|
||||
<icon-octicon-pin-16 v-if="item.fixed === 'unFixed'" />
|
||||
<icon-octicon-pin-16 v-else-if="item.fixed === 'left'" class="rotate-270" />
|
||||
<icon-octicon-pin-slash-16 v-else />
|
||||
</ButtonIcon>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
</NPopover>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -266,6 +266,7 @@ function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||
key: column.key as string,
|
||||
title: column.title!,
|
||||
checked: true,
|
||||
fixed: column.fixed ?? 'unFixed',
|
||||
visible: getColumnVisible?.(column) ?? true
|
||||
});
|
||||
} else if (column.type === 'selection') {
|
||||
@@ -273,6 +274,7 @@ function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||
key: SELECTION_KEY,
|
||||
title: $t('common.check'),
|
||||
checked: true,
|
||||
fixed: column.fixed ?? 'unFixed',
|
||||
visible: getColumnVisible?.(column) ?? false
|
||||
});
|
||||
} else if (column.type === 'expand') {
|
||||
@@ -280,6 +282,7 @@ function getColumnChecks<Column extends NaiveUI.TableColumn<any>>(
|
||||
key: EXPAND_KEY,
|
||||
title: $t('common.expandColumn'),
|
||||
checked: true,
|
||||
fixed: column.fixed ?? 'unFixed',
|
||||
visible: getColumnVisible?.(column) ?? false
|
||||
});
|
||||
}
|
||||
@@ -301,7 +304,14 @@ function getColumns<Column extends NaiveUI.TableColumn<any>>(cols: Column[], che
|
||||
}
|
||||
});
|
||||
|
||||
const filteredColumns = checks.filter(item => item.checked).map(check => columnMap.get(check.key) as Column);
|
||||
const filteredColumns = checks
|
||||
.filter(item => item.checked)
|
||||
.map(check => {
|
||||
return {
|
||||
...columnMap.get(check.key),
|
||||
fixed: check.fixed
|
||||
} as Column;
|
||||
});
|
||||
|
||||
return filteredColumns;
|
||||
}
|
||||
|
||||
@@ -24,3 +24,7 @@ export const $t = i18n.global.t as App.I18n.$T;
|
||||
export function setLocale(locale: App.I18n.LangType) {
|
||||
i18n.global.locale.value = locale;
|
||||
}
|
||||
|
||||
export function getLocale(): App.I18n.LangType {
|
||||
return i18n.global.locale.value as App.I18n.LangType;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ const local: App.I18n.Schema = {
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
check: 'Check',
|
||||
selectAll: 'Select All',
|
||||
expandColumn: 'Expand Column',
|
||||
columnSetting: 'Column Setting',
|
||||
config: 'Config',
|
||||
@@ -356,7 +357,12 @@ const local: App.I18n.Schema = {
|
||||
unpin: 'Unpin'
|
||||
},
|
||||
datatable: {
|
||||
itemCount: 'Total {total} items'
|
||||
itemCount: 'Total {total} items',
|
||||
fixed: {
|
||||
left: 'Left Fixed',
|
||||
right: 'Right Fixed',
|
||||
unFixed: 'Unfixed'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const local: App.I18n.Schema = {
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
check: '勾选',
|
||||
selectAll: '全选',
|
||||
expandColumn: '展开列',
|
||||
columnSetting: '列设置',
|
||||
config: '配置',
|
||||
@@ -352,7 +353,12 @@ const local: App.I18n.Schema = {
|
||||
unpin: '取消固定'
|
||||
},
|
||||
datatable: {
|
||||
itemCount: '共 {total} 条'
|
||||
itemCount: '共 {total} 条',
|
||||
fixed: {
|
||||
left: '左固定',
|
||||
right: '右固定',
|
||||
unFixed: '取消固定'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import './plugins/assets';
|
||||
import { setupVueRootValidator } from 'vite-plugin-vue-transition-root-validator/client';
|
||||
import { setupAppVersionNotification, setupDayjs, setupIconifyOffline, setupLoading, setupNProgress } from './plugins';
|
||||
import { setupStore } from './store';
|
||||
import { setupRouter } from './router';
|
||||
import { setupI18n } from './locales';
|
||||
import { getLocale, setupI18n } from './locales';
|
||||
import App from './App.vue';
|
||||
|
||||
async function setupApp() {
|
||||
@@ -25,6 +26,10 @@ async function setupApp() {
|
||||
|
||||
setupAppVersionNotification();
|
||||
|
||||
setupVueRootValidator(app, {
|
||||
lang: getLocale() === 'zh-CN' ? 'zh' : 'en'
|
||||
});
|
||||
|
||||
app.mount('#app');
|
||||
}
|
||||
|
||||
|
||||
7
src/typings/app.d.ts
vendored
7
src/typings/app.d.ts
vendored
@@ -196,6 +196,7 @@ declare namespace App {
|
||||
type RouterPushOptions = {
|
||||
query?: Record<string, string>;
|
||||
params?: Record<string, string>;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
/** The global header props */
|
||||
@@ -326,6 +327,7 @@ declare namespace App {
|
||||
cancel: string;
|
||||
close: string;
|
||||
check: string;
|
||||
selectAll: string;
|
||||
expandColumn: string;
|
||||
columnSetting: string;
|
||||
config: string;
|
||||
@@ -578,6 +580,11 @@ declare namespace App {
|
||||
};
|
||||
datatable: {
|
||||
itemCount: string;
|
||||
fixed: {
|
||||
left: string;
|
||||
right: string;
|
||||
unFixed: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
1
src/typings/naive-ui.d.ts
vendored
1
src/typings/naive-ui.d.ts
vendored
@@ -7,6 +7,7 @@ declare namespace NaiveUI {
|
||||
type DataTableSelectionColumn<T> = import('naive-ui').DataTableSelectionColumn<T>;
|
||||
type TableColumnGroup<T> = import('naive-ui/es/data-table/src/interface').TableColumnGroup<T>;
|
||||
type TableColumnCheck = import('@sa/hooks').TableColumnCheck;
|
||||
type TableColumnFixed = import('@sa/hooks').TableColumnCheck['fixed'];
|
||||
|
||||
type SetTableColumnKey<C, T> = Omit<C, 'key'> & { key: keyof T | (string & {}) };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user