mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat(site): save sync node ids
This commit is contained in:
parent
aa556767f2
commit
6c137e5229
21 changed files with 381 additions and 327 deletions
0
.db
0
.db
|
@ -6,6 +6,7 @@ func InitRouter(r *gin.RouterGroup) {
|
||||||
r.GET("domains", GetSiteList)
|
r.GET("domains", GetSiteList)
|
||||||
r.GET("domains/:name", GetSite)
|
r.GET("domains/:name", GetSite)
|
||||||
r.POST("domains/:name", SaveSite)
|
r.POST("domains/:name", SaveSite)
|
||||||
|
r.PUT("domains", BatchUpdateSites)
|
||||||
r.POST("domains/:name/enable", EnableSite)
|
r.POST("domains/:name/enable", EnableSite)
|
||||||
r.POST("domains/:name/disable", DisableSite)
|
r.POST("domains/:name/disable", DisableSite)
|
||||||
r.POST("domains/:name/advance", DomainEditByAdvancedMode)
|
r.POST("domains/:name/advance", DomainEditByAdvancedMode)
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"github.com/0xJacky/Nginx-UI/query"
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
|
"github.com/uozi-tech/cosy"
|
||||||
"github.com/uozi-tech/cosy/logger"
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
@ -125,10 +127,11 @@ func SaveSite(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var json struct {
|
var json struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
Content string `json:"content" binding:"required"`
|
Content string `json:"content" binding:"required"`
|
||||||
SiteCategoryID uint64 `json:"site_category_id"`
|
SiteCategoryID uint64 `json:"site_category_id"`
|
||||||
Overwrite bool `json:"overwrite"`
|
SyncNodeIDs []uint64 `json:"sync_node_ids"`
|
||||||
|
Overwrite bool `json:"overwrite"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !api.BindAndValid(c, &json) {
|
if !api.BindAndValid(c, &json) {
|
||||||
|
@ -152,7 +155,12 @@ func SaveSite(c *gin.Context) {
|
||||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
||||||
s := query.Site
|
s := query.Site
|
||||||
|
|
||||||
_, err = s.Where(s.Path.Eq(path)).Update(s.SiteCategoryID, json.SiteCategoryID)
|
_, err = s.Where(s.Path.Eq(path)).
|
||||||
|
Select(s.SiteCategoryID, s.SyncNodeIDs).
|
||||||
|
Updates(&model.Site{
|
||||||
|
SiteCategoryID: json.SiteCategoryID,
|
||||||
|
SyncNodeIDs: json.SyncNodeIDs,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
@ -336,3 +344,29 @@ func DeleteSite(c *gin.Context) {
|
||||||
"message": "ok",
|
"message": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BatchUpdateSites(c *gin.Context) {
|
||||||
|
cosy.Core[model.Site](c).SetValidRules(gin.H{
|
||||||
|
"site_category_id": "required",
|
||||||
|
}).SetItemKey("path").
|
||||||
|
BeforeExecuteHook(func(ctx *cosy.Ctx[model.Site]) {
|
||||||
|
effectedPath := make([]string, len(ctx.BatchEffectedIDs))
|
||||||
|
var sites []*model.Site
|
||||||
|
for i, name := range ctx.BatchEffectedIDs {
|
||||||
|
path := nginx.GetConfPath("sites-available", name)
|
||||||
|
effectedPath[i] = path
|
||||||
|
sites = append(sites, &model.Site{
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
s := query.Site
|
||||||
|
err := s.Clauses(clause.OnConflict{
|
||||||
|
DoNothing: true,
|
||||||
|
}).Create(sites...)
|
||||||
|
if err != nil {
|
||||||
|
ctx.AbortWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.BatchEffectedIDs = effectedPath
|
||||||
|
}).BatchModify()
|
||||||
|
}
|
|
@ -1,95 +1,95 @@
|
||||||
export default {
|
export default {
|
||||||
globals: {
|
"globals": {
|
||||||
$gettext: true,
|
"$gettext": true,
|
||||||
$ngettext: true,
|
"$ngettext": true,
|
||||||
$npgettext: true,
|
"$npgettext": true,
|
||||||
$pgettext: true,
|
"$pgettext": true,
|
||||||
Component: true,
|
"Component": true,
|
||||||
ComponentPublicInstance: true,
|
"ComponentPublicInstance": true,
|
||||||
ComputedRef: true,
|
"ComputedRef": true,
|
||||||
DirectiveBinding: true,
|
"DirectiveBinding": true,
|
||||||
EffectScope: true,
|
"EffectScope": true,
|
||||||
ExtractDefaultPropTypes: true,
|
"ExtractDefaultPropTypes": true,
|
||||||
ExtractPropTypes: true,
|
"ExtractPropTypes": true,
|
||||||
ExtractPublicPropTypes: true,
|
"ExtractPublicPropTypes": true,
|
||||||
InjectionKey: true,
|
"InjectionKey": true,
|
||||||
MaybeRef: true,
|
"MaybeRef": true,
|
||||||
MaybeRefOrGetter: true,
|
"MaybeRefOrGetter": true,
|
||||||
PropType: true,
|
"PropType": true,
|
||||||
Ref: true,
|
"Ref": true,
|
||||||
VNode: true,
|
"VNode": true,
|
||||||
WritableComputedRef: true,
|
"WritableComputedRef": true,
|
||||||
acceptHMRUpdate: true,
|
"acceptHMRUpdate": true,
|
||||||
computed: true,
|
"computed": true,
|
||||||
createApp: true,
|
"createApp": true,
|
||||||
createPinia: true,
|
"createPinia": true,
|
||||||
customRef: true,
|
"customRef": true,
|
||||||
defineAsyncComponent: true,
|
"defineAsyncComponent": true,
|
||||||
defineComponent: true,
|
"defineComponent": true,
|
||||||
defineStore: true,
|
"defineStore": true,
|
||||||
effectScope: true,
|
"effectScope": true,
|
||||||
getActivePinia: true,
|
"getActivePinia": true,
|
||||||
getCurrentInstance: true,
|
"getCurrentInstance": true,
|
||||||
getCurrentScope: true,
|
"getCurrentScope": true,
|
||||||
h: true,
|
"h": true,
|
||||||
inject: true,
|
"inject": true,
|
||||||
isProxy: true,
|
"isProxy": true,
|
||||||
isReactive: true,
|
"isReactive": true,
|
||||||
isReadonly: true,
|
"isReadonly": true,
|
||||||
isRef: true,
|
"isRef": true,
|
||||||
mapActions: true,
|
"mapActions": true,
|
||||||
mapGetters: true,
|
"mapGetters": true,
|
||||||
mapState: true,
|
"mapState": true,
|
||||||
mapStores: true,
|
"mapStores": true,
|
||||||
mapWritableState: true,
|
"mapWritableState": true,
|
||||||
markRaw: true,
|
"markRaw": true,
|
||||||
nextTick: true,
|
"nextTick": true,
|
||||||
onActivated: true,
|
"onActivated": true,
|
||||||
onBeforeMount: true,
|
"onBeforeMount": true,
|
||||||
onBeforeRouteLeave: true,
|
"onBeforeRouteLeave": true,
|
||||||
onBeforeRouteUpdate: true,
|
"onBeforeRouteUpdate": true,
|
||||||
onBeforeUnmount: true,
|
"onBeforeUnmount": true,
|
||||||
onBeforeUpdate: true,
|
"onBeforeUpdate": true,
|
||||||
onDeactivated: true,
|
"onDeactivated": true,
|
||||||
onErrorCaptured: true,
|
"onErrorCaptured": true,
|
||||||
onMounted: true,
|
"onMounted": true,
|
||||||
onRenderTracked: true,
|
"onRenderTracked": true,
|
||||||
onRenderTriggered: true,
|
"onRenderTriggered": true,
|
||||||
onScopeDispose: true,
|
"onScopeDispose": true,
|
||||||
onServerPrefetch: true,
|
"onServerPrefetch": true,
|
||||||
onUnmounted: true,
|
"onUnmounted": true,
|
||||||
onUpdated: true,
|
"onUpdated": true,
|
||||||
onWatcherCleanup: true,
|
"onWatcherCleanup": true,
|
||||||
provide: true,
|
"provide": true,
|
||||||
reactive: true,
|
"reactive": true,
|
||||||
readonly: true,
|
"readonly": true,
|
||||||
ref: true,
|
"ref": true,
|
||||||
resolveComponent: true,
|
"resolveComponent": true,
|
||||||
setActivePinia: true,
|
"setActivePinia": true,
|
||||||
setMapStoreSuffix: true,
|
"setMapStoreSuffix": true,
|
||||||
shallowReactive: true,
|
"shallowReactive": true,
|
||||||
shallowReadonly: true,
|
"shallowReadonly": true,
|
||||||
shallowRef: true,
|
"shallowRef": true,
|
||||||
storeToRefs: true,
|
"storeToRefs": true,
|
||||||
toRaw: true,
|
"toRaw": true,
|
||||||
toRef: true,
|
"toRef": true,
|
||||||
toRefs: true,
|
"toRefs": true,
|
||||||
toValue: true,
|
"toValue": true,
|
||||||
triggerRef: true,
|
"triggerRef": true,
|
||||||
unref: true,
|
"unref": true,
|
||||||
useAttrs: true,
|
"useAttrs": true,
|
||||||
useCssModule: true,
|
"useCssModule": true,
|
||||||
useCssVars: true,
|
"useCssVars": true,
|
||||||
useId: true,
|
"useId": true,
|
||||||
useLink: true,
|
"useLink": true,
|
||||||
useModel: true,
|
"useModel": true,
|
||||||
useRoute: true,
|
"useRoute": true,
|
||||||
useRouter: true,
|
"useRouter": true,
|
||||||
useSlots: true,
|
"useSlots": true,
|
||||||
useTemplateRef: true,
|
"useTemplateRef": true,
|
||||||
watch: true,
|
"watch": true,
|
||||||
watchEffect: true,
|
"watchEffect": true,
|
||||||
watchPostEffect: true,
|
"watchPostEffect": true,
|
||||||
watchSyncEffect: true,
|
"watchSyncEffect": true
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
2
app/components.d.ts
vendored
2
app/components.d.ts
vendored
|
@ -49,6 +49,8 @@ declare module 'vue' {
|
||||||
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
|
||||||
APopover: typeof import('ant-design-vue/es')['Popover']
|
APopover: typeof import('ant-design-vue/es')['Popover']
|
||||||
AProgress: typeof import('ant-design-vue/es')['Progress']
|
AProgress: typeof import('ant-design-vue/es')['Progress']
|
||||||
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
AResult: typeof import('ant-design-vue/es')['Result']
|
AResult: typeof import('ant-design-vue/es')['Result']
|
||||||
ARow: typeof import('ant-design-vue/es')['Row']
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
|
|
|
@ -5,7 +5,7 @@ import autoImport from './.eslint-auto-import.mjs'
|
||||||
export default createConfig(
|
export default createConfig(
|
||||||
{
|
{
|
||||||
stylistic: true,
|
stylistic: true,
|
||||||
ignores: ['**/version.json', 'tsconfig.json', 'tsconfig.node.json'],
|
ignores: ['**/version.json', 'tsconfig.json', 'tsconfig.node.json', '.eslint-auto-import.mjs'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: autoImport.globals,
|
globals: autoImport.globals,
|
||||||
},
|
},
|
||||||
|
|
|
@ -19,6 +19,7 @@ export interface Site {
|
||||||
cert_info?: Record<number, CertificateInfo[]>
|
cert_info?: Record<number, CertificateInfo[]>
|
||||||
site_category_id: number
|
site_category_id: number
|
||||||
site_category?: SiteCategory
|
site_category?: SiteCategory
|
||||||
|
sync_node_ids: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutoCertRequest {
|
export interface AutoCertRequest {
|
||||||
|
|
|
@ -1,48 +1,60 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type Curd from '@/api/curd'
|
||||||
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
|
import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
|
||||||
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
api: (ids: number[], data: any) => Promise<void>
|
api: Curd<any>
|
||||||
beforeSave?: () => Promise<void>
|
beforeSave?: () => Promise<void>
|
||||||
|
columns: Column[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['onSave'])
|
const emit = defineEmits(['save'])
|
||||||
|
|
||||||
const batchColumns = ref([])
|
const batchColumns = ref<Column[]>([])
|
||||||
|
const selectedRowKeys = ref<(number | string)[]>([])
|
||||||
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
const selectedRows = ref<any[]>([])
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
|
const data = ref({})
|
||||||
|
const error = ref({})
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
const selectedRowKeys = ref([])
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
function showModal(c: any, rowKeys: any) {
|
function showModal(c: Column[], rowKeys: (number | string)[], rows: any[]) {
|
||||||
|
data.value = {}
|
||||||
visible.value = true
|
visible.value = true
|
||||||
selectedRowKeys.value = rowKeys
|
selectedRowKeys.value = rowKeys
|
||||||
batchColumns.value = c
|
batchColumns.value = c
|
||||||
|
selectedRows.value = rows
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
showModal,
|
showModal,
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = reactive({})
|
|
||||||
const error = reactive({})
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
async function ok() {
|
async function ok() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
await props.beforeSave?.()
|
await props.beforeSave?.()
|
||||||
|
|
||||||
await props.api(selectedRowKeys.value, data).then(async () => {
|
await props.api.batch_save(selectedRowKeys.value, data.value)
|
||||||
message.success($gettext('Save successfully'))
|
.then(async () => {
|
||||||
emit('onSave')
|
message.success($gettext('Save successfully'))
|
||||||
}).catch(e => {
|
emit('save')
|
||||||
message.error($gettext(e?.message) ?? $gettext('Server error'))
|
visible.value = false
|
||||||
}).finally(() => {
|
})
|
||||||
loading.value = false
|
.catch(e => {
|
||||||
})
|
error.value = e.errors
|
||||||
|
message.error($gettext(e?.message) ?? $gettext('Server error'))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -52,23 +64,31 @@ async function ok() {
|
||||||
class="std-curd-edit-modal"
|
class="std-curd-edit-modal"
|
||||||
:mask="false"
|
:mask="false"
|
||||||
:title="$gettext('Batch Modify')"
|
:title="$gettext('Batch Modify')"
|
||||||
:cancel-text="$gettext('Cancel')"
|
:cancel-text="$gettext('No')"
|
||||||
:ok-text="$gettext('OK')"
|
:ok-text="$gettext('Save')"
|
||||||
:confirm-loading="loading"
|
:confirm-loading="loading"
|
||||||
:width="600"
|
:width="600"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
@ok="ok"
|
@ok="ok"
|
||||||
>
|
>
|
||||||
|
<p>{{ $gettext('Belows are selected items that you want to batch modify') }}</p>
|
||||||
|
<ATable
|
||||||
|
class="mb-4"
|
||||||
|
size="small"
|
||||||
|
:columns="getPithyColumns(columns)"
|
||||||
|
:data-source="selectedRows"
|
||||||
|
:pagination="{ showSizeChanger: false, pageSize: 5, size: 'small' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p>{{ $gettext('Leave blank if do not want to modify') }}</p>
|
||||||
<StdDataEntry
|
<StdDataEntry
|
||||||
:data-list="batchColumns"
|
:data-list="batchColumns"
|
||||||
:data-source="data"
|
:data-source="data"
|
||||||
:error="error"
|
:errors="error"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
</AModal>
|
</AModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts" generic="T=any">
|
<script setup lang="ts" generic="T=any">
|
||||||
import type { StdTableSlots } from '@/components/StdDesign/StdDataDisplay/types'
|
|
||||||
import type { Column } from '@/components/StdDesign/types'
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
import type { StdTableProps } from './StdTable.vue'
|
import type { StdTableProps } from './StdTable.vue'
|
||||||
|
import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
||||||
import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
|
import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
|
||||||
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
@ -14,7 +14,7 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
|
||||||
modalMask?: boolean
|
modalMask?: boolean
|
||||||
exportExcel?: boolean
|
exportExcel?: boolean
|
||||||
importExcel?: boolean
|
importExcel?: boolean
|
||||||
disableTrash?: boolean
|
|
||||||
disableAdd?: boolean
|
disableAdd?: boolean
|
||||||
onClickAdd?: () => void
|
onClickAdd?: () => void
|
||||||
|
|
||||||
|
@ -24,6 +24,10 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<StdTableProps<T> & StdCurdProps<T>>()
|
const props = defineProps<StdTableProps<T> & StdCurdProps<T>>()
|
||||||
|
|
||||||
|
const selectedRowKeys = ref<(string | number)[]>([])
|
||||||
|
const selectedRows: Ref<T[]> = ref([])
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
const data: any = reactive({ id: null })
|
const data: any = reactive({ id: null })
|
||||||
|
@ -61,24 +65,13 @@ function add(preset: any = undefined) {
|
||||||
if (preset)
|
if (preset)
|
||||||
Object.assign(data, preset)
|
Object.assign(data, preset)
|
||||||
|
|
||||||
clear_error()
|
clearError()
|
||||||
visible.value = true
|
visible.value = true
|
||||||
editMode.value = 'create'
|
editMode.value = 'create'
|
||||||
modifyMode.value = true
|
modifyMode.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = ref()
|
const table = useTemplateRef('table')
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
|
|
||||||
default: () => [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
const selectedRows = defineModel<any[]>('selectedRows', {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const getParams = reactive({
|
const getParams = reactive({
|
||||||
trash: false,
|
trash: false,
|
||||||
|
@ -101,20 +94,23 @@ defineExpose({
|
||||||
setParams,
|
setParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
function clear_error() {
|
function clearError() {
|
||||||
Object.keys(error).forEach(v => {
|
Object.keys(error).forEach(v => {
|
||||||
delete error[v]
|
delete error[v]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdEntryRef = ref()
|
const stdEntryRef = useTemplateRef('stdEntryRef')
|
||||||
|
|
||||||
async function ok() {
|
async function ok() {
|
||||||
|
if (!stdEntryRef.value)
|
||||||
|
return
|
||||||
|
|
||||||
const { formRef } = stdEntryRef.value
|
const { formRef } = stdEntryRef.value
|
||||||
|
|
||||||
clear_error()
|
clearError()
|
||||||
try {
|
try {
|
||||||
await formRef.validateFields()
|
await formRef?.validateFields()
|
||||||
props?.beforeSave?.(data)
|
props?.beforeSave?.(data)
|
||||||
props
|
props
|
||||||
.api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => {
|
.api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => {
|
||||||
|
@ -135,7 +131,7 @@ async function ok() {
|
||||||
function cancel() {
|
function cancel() {
|
||||||
visible.value = false
|
visible.value = false
|
||||||
|
|
||||||
clear_error()
|
clearError()
|
||||||
|
|
||||||
if (shouldRefetchList.value) {
|
if (shouldRefetchList.value) {
|
||||||
get_list()
|
get_list()
|
||||||
|
@ -159,7 +155,6 @@ function view(id: number | string) {
|
||||||
get(id).then(() => {
|
get(id).then(() => {
|
||||||
visible.value = true
|
visible.value = true
|
||||||
modifyMode.value = false
|
modifyMode.value = false
|
||||||
editMode.value = 'modify'
|
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
message.error($gettext(e?.message ?? 'Server error'), 5)
|
message.error($gettext(e?.message ?? 'Server error'), 5)
|
||||||
})
|
})
|
||||||
|
@ -183,6 +178,17 @@ const modalTitle = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const localOverwriteParams = reactive(props.overwriteParams ?? {})
|
const localOverwriteParams = reactive(props.overwriteParams ?? {})
|
||||||
|
|
||||||
|
const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
|
||||||
|
|
||||||
|
async function handleClickBatchEdit(batchColumns: Column[]) {
|
||||||
|
stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys.value, selectedRows.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchUpdated() {
|
||||||
|
table.value?.get_list()
|
||||||
|
table.value?.resetSelection()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -202,7 +208,7 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
|
||||||
@click="add"
|
@click="add"
|
||||||
>{{ $gettext('Add') }}</a>
|
>{{ $gettext('Add') }}</a>
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
<template v-if="!disableDelete && !disableTrash">
|
<template v-if="!disableDelete">
|
||||||
<a
|
<a
|
||||||
v-if="!getParams.trash"
|
v-if="!getParams.trash"
|
||||||
@click="getParams.trash = true"
|
@click="getParams.trash = true"
|
||||||
|
@ -219,21 +225,23 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
|
||||||
</ASpace>
|
</ASpace>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<slot name="beforeTable" />
|
||||||
<StdTable
|
<StdTable
|
||||||
ref="table"
|
ref="table"
|
||||||
v-model:selected-row-keys="selectedRowKeys"
|
|
||||||
v-model:selected-rows="selectedRows"
|
|
||||||
v-bind="{
|
v-bind="{
|
||||||
...props,
|
...props,
|
||||||
getParams,
|
getParams,
|
||||||
overwriteParams: localOverwriteParams,
|
overwriteParams: localOverwriteParams,
|
||||||
}"
|
}"
|
||||||
|
v-model:selected-row-keys="selectedRowKeys"
|
||||||
|
v-model:selected-rows="selectedRows"
|
||||||
@click-edit="edit"
|
@click-edit="edit"
|
||||||
@click-view="view"
|
@click-view="view"
|
||||||
@selected="onSelect"
|
@selected="onSelect"
|
||||||
|
@click-batch-modify="handleClickBatchEdit"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="(_, key) in ($slots as unknown as StdTableSlots)"
|
v-for="(_, key) in $slots"
|
||||||
:key="key"
|
:key="key"
|
||||||
#[key]="slotProps"
|
#[key]="slotProps"
|
||||||
>
|
>
|
||||||
|
@ -295,10 +303,17 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
|
||||||
|
|
||||||
<StdCurdDetail
|
<StdCurdDetail
|
||||||
v-else
|
v-else
|
||||||
:columns="columns"
|
:columns
|
||||||
:data="data"
|
:data
|
||||||
/>
|
/>
|
||||||
</AModal>
|
</AModal>
|
||||||
|
|
||||||
|
<StdBatchEdit
|
||||||
|
ref="stdBatchEditRef"
|
||||||
|
:api
|
||||||
|
:columns
|
||||||
|
@save="handleBatchUpdated"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,11 @@ import type { FilterValue } from 'ant-design-vue/es/table/interface'
|
||||||
import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
|
import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
|
||||||
import type { ComputedRef, Ref } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
import type { RouteParams } from 'vue-router'
|
import type { RouteParams } from 'vue-router'
|
||||||
|
import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
|
||||||
import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
|
import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
|
||||||
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
|
||||||
import { HolderOutlined } from '@ant-design/icons-vue'
|
import { HolderOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { watchPausable } from '@vueuse/core'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import StdPagination from './StdPagination.vue'
|
import StdPagination from './StdPagination.vue'
|
||||||
|
@ -20,6 +22,7 @@ export interface StdTableProps<T = any> {
|
||||||
title?: string
|
title?: string
|
||||||
mode?: string
|
mode?: string
|
||||||
rowKey?: string
|
rowKey?: string
|
||||||
|
|
||||||
api: Curd<T>
|
api: Curd<T>
|
||||||
columns: Column[]
|
columns: Column[]
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
@ -48,7 +51,20 @@ const props = withDefaults(defineProps<StdTableProps<T>>(), {
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['clickEdit', 'clickView', 'clickBatchModify', 'update:selectedRowKeys'])
|
const emit = defineEmits([
|
||||||
|
'clickEdit',
|
||||||
|
'clickView',
|
||||||
|
'clickBatchModify',
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectedRowKeys = defineModel<(number | string)[]>('selectedRowKeys', {
|
||||||
|
default: () => reactive([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedRows = defineModel<T[]>('selectedRows', {
|
||||||
|
default: () => reactive([]),
|
||||||
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const dataSource: Ref<T[]> = ref([])
|
const dataSource: Ref<T[]> = ref([])
|
||||||
|
@ -93,17 +109,6 @@ const params = reactive({
|
||||||
...props.getParams,
|
...props.getParams,
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
|
|
||||||
default: () => [],
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
const selectedRows = defineModel<any[]>('selectedRows', {
|
|
||||||
type: Array,
|
|
||||||
default: () => [],
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
selectedRows.value.forEach(v => {
|
selectedRows.value.forEach(v => {
|
||||||
selectedRecords.value[v[props.rowKey]] = v
|
selectedRecords.value[v[props.rowKey]] = v
|
||||||
|
@ -122,7 +127,9 @@ const searchColumns = computed(() => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
else { _searchColumns.push({ ...column }) }
|
else {
|
||||||
|
_searchColumns.push({ ...column })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -130,11 +137,8 @@ const searchColumns = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const pithyColumns = computed<Column[]>(() => {
|
const pithyColumns = computed<Column[]>(() => {
|
||||||
if (props.pithy) {
|
if (props.pithy)
|
||||||
return props.columns?.filter(c => {
|
return getPithyColumns(props.columns)
|
||||||
return c.pithy === true && !c.hiddenInTable
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return props.columns?.filter(c => {
|
return props.columns?.filter(c => {
|
||||||
return !c.hiddenInTable
|
return !c.hiddenInTable
|
||||||
|
@ -142,19 +146,12 @@ const pithyColumns = computed<Column[]>(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const batchColumns = computed(() => {
|
const batchColumns = computed(() => {
|
||||||
const batch: Column[] = []
|
return props.columns?.filter(column => column.batch) || []
|
||||||
|
|
||||||
props.columns?.forEach(column => {
|
|
||||||
if (column.batch)
|
|
||||||
batch.push(column)
|
|
||||||
})
|
|
||||||
|
|
||||||
return batch
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const get_list = _.debounce(_get_list, 100, {
|
const get_list = _.debounce(_get_list, 100, {
|
||||||
leading: false,
|
leading: true,
|
||||||
trailing: true,
|
trailing: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filterParams = reactive({})
|
const filterParams = reactive({})
|
||||||
|
@ -184,15 +181,13 @@ onMounted(() => {
|
||||||
if (props.sortable)
|
if (props.sortable)
|
||||||
initSortable()
|
initSortable()
|
||||||
|
|
||||||
if (!selectedRowKeys.value?.length)
|
|
||||||
selectedRowKeys.value = []
|
|
||||||
|
|
||||||
init.value = true
|
init.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
get_list,
|
get_list,
|
||||||
pagination,
|
pagination,
|
||||||
|
resetSelection,
|
||||||
})
|
})
|
||||||
|
|
||||||
function destroy(id: number | string) {
|
function destroy(id: number | string) {
|
||||||
|
@ -229,13 +224,17 @@ function buildIndexMap(data: any, level: number = 0, index: number = 0, total: n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _get_list(page_num = null, page_size = 20) {
|
async function _get_list(page_num: number | null = null, page_size = 20) {
|
||||||
dataSource.value = []
|
dataSource.value = []
|
||||||
loading.value = true
|
loading.value = true
|
||||||
if (page_num) {
|
if (page_num) {
|
||||||
params.page = page_num
|
params.page = page_num
|
||||||
params.page_size = page_size
|
params.page_size = page_size
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
params.page = 1
|
||||||
|
params.page_size = page_size
|
||||||
|
}
|
||||||
props.api?.get_list({ ...params, ...props.overwriteParams }).then(async r => {
|
props.api?.get_list({ ...params, ...props.overwriteParams }).then(async r => {
|
||||||
dataSource.value = r.data
|
dataSource.value = r.data
|
||||||
rowsKeyIndexMap.value = {}
|
rowsKeyIndexMap.value = {}
|
||||||
|
@ -245,9 +244,7 @@ async function _get_list(page_num = null, page_size = 20) {
|
||||||
if (r.pagination)
|
if (r.pagination)
|
||||||
Object.assign(pagination, r.pagination)
|
Object.assign(pagination, r.pagination)
|
||||||
|
|
||||||
setTimeout(() => {
|
loading.value = false
|
||||||
loading.value = false
|
|
||||||
}, 200)
|
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
message.error(e?.message ?? $gettext('Server error'))
|
message.error(e?.message ?? $gettext('Server error'))
|
||||||
})
|
})
|
||||||
|
@ -288,7 +285,8 @@ function expandedTable(keys: Key[]) {
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
|
async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
|
||||||
if (props.selectionType === 'checkbox' || props.exportExcel) {
|
// console.log('onSelect', record, selected, _selectedRows)
|
||||||
|
if (props.selectionType === 'checkbox' || batchColumns.value.length > 0 || props.exportExcel) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
_selectedRows.forEach(v => {
|
_selectedRows.forEach(v => {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
@ -300,20 +298,11 @@ async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
selectedRowKeys.value.splice(selectedRowKeys.value.indexOf(record[props.rowKey]), 1)
|
||||||
selectedRowKeys.value = selectedRowKeys.value.filter((v: any) => v !== record[props.rowKey])
|
|
||||||
delete selectedRecords.value[record[props.rowKey]]
|
delete selectedRecords.value[record[props.rowKey]]
|
||||||
}
|
}
|
||||||
|
await nextTick()
|
||||||
await nextTick(async () => {
|
selectedRows.value = [...selectedRowKeys.value.map(v => selectedRecords.value[v])]
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
const filteredRows: any[] = []
|
|
||||||
|
|
||||||
selectedRowKeys.value.forEach(v => {
|
|
||||||
filteredRows.push(selectedRecords.value[v])
|
|
||||||
})
|
|
||||||
selectedRows.value = filteredRows
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
else if (selected) {
|
else if (selected) {
|
||||||
selectedRowKeys.value = record[props.rowKey]
|
selectedRowKeys.value = record[props.rowKey]
|
||||||
|
@ -327,7 +316,7 @@ async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
|
||||||
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
|
async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
|
||||||
// console.log(selected, selectedRows, changeRows)
|
// console.log('onSelectAll', selected, selectedRows, changeRows)
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
changeRows.forEach((v: any) => {
|
changeRows.forEach((v: any) => {
|
||||||
if (v) {
|
if (v) {
|
||||||
|
@ -342,22 +331,19 @@ async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows:
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
selectedRowKeys.value = selectedRowKeys.value.filter(v => {
|
selectedRowKeys.value.splice(0, selectedRowKeys.value.length, ...selectedRowKeys.value.filter(v => selectedRecords.value[v]))
|
||||||
return selectedRecords.value[v]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(selectedRowKeysBuffer.value, selectedRecords.value)
|
// console.log(selectedRowKeysBuffer.value, selectedRecords.value)
|
||||||
|
|
||||||
await nextTick(async () => {
|
await nextTick()
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
selectedRows.value.splice(0, selectedRows.value.length, ...selectedRowKeys.value.map(v => selectedRecords.value[v]))
|
||||||
const filteredRows: any[] = []
|
}
|
||||||
|
|
||||||
selectedRowKeys.value.forEach(v => {
|
function resetSelection() {
|
||||||
filteredRows.push(selectedRecords.value[v])
|
selectedRowKeys.value = reactive([])
|
||||||
})
|
selectedRows.value = reactive([])
|
||||||
selectedRows.value = filteredRows
|
selectedRecords.value = reactive({})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -381,7 +367,7 @@ async function resetSearch() {
|
||||||
updateFilter.value++
|
updateFilter.value++
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(params, v => {
|
const { stop: stopWatchParams, resume: resumeWatchParams } = watchPausable(params, v => {
|
||||||
if (!init.value)
|
if (!init.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -393,7 +379,6 @@ watch(params, v => {
|
||||||
|
|
||||||
watch(() => route.query, async () => {
|
watch(() => route.query, async () => {
|
||||||
params.trash = route.query.trash === 'true'
|
params.trash = route.query.trash === 'true'
|
||||||
params.team_id = route.query.team_id
|
|
||||||
|
|
||||||
if (init.value)
|
if (init.value)
|
||||||
await get_list()
|
await get_list()
|
||||||
|
@ -425,14 +410,16 @@ if (props.overwriteParams) {
|
||||||
const rowSelection = computed(() => {
|
const rowSelection = computed(() => {
|
||||||
if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
|
if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
|
||||||
return {
|
return {
|
||||||
selectedRowKeys: selectedRowKeys.value,
|
selectedRowKeys: unref(selectedRowKeys),
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
getCheckboxProps: props?.getCheckboxProps,
|
getCheckboxProps: props?.getCheckboxProps,
|
||||||
type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
|
type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else { return null }
|
else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
}) as ComputedRef<TableProps['rowSelection']>
|
}) as ComputedRef<TableProps['rowSelection']>
|
||||||
|
|
||||||
const hasSelectedRow = computed(() => {
|
const hasSelectedRow = computed(() => {
|
||||||
|
@ -440,18 +427,21 @@ const hasSelectedRow = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function clickBatchEdit() {
|
function clickBatchEdit() {
|
||||||
emit('clickBatchModify', batchColumns.value, selectedRowKeys.value)
|
emit('clickBatchModify', batchColumns.value, selectedRowKeys.value, selectedRows.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initSortable() {
|
function initSortable() {
|
||||||
useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
|
useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePage(page: number, page_size: number) {
|
async function changePage(page: number, page_size: number) {
|
||||||
|
stopWatchParams()
|
||||||
Object.assign(params, {
|
Object.assign(params, {
|
||||||
page,
|
page,
|
||||||
page_size,
|
page_size,
|
||||||
})
|
})
|
||||||
|
resumeWatchParams()
|
||||||
|
await get_list(page, page_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paginationSize = computed(() => {
|
const paginationSize = computed(() => {
|
||||||
|
@ -529,10 +519,7 @@ const paginationSize = computed(() => {
|
||||||
>
|
>
|
||||||
{{ $gettext('Modify') }}
|
{{ $gettext('Modify') }}
|
||||||
</AButton>
|
</AButton>
|
||||||
<ADivider
|
<ADivider type="vertical" />
|
||||||
v-if="!props.disableDelete"
|
|
||||||
type="vertical"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot
|
<slot
|
||||||
|
@ -569,6 +556,7 @@ const paginationSize = computed(() => {
|
||||||
{{ $gettext('Recover') }}
|
{{ $gettext('Recover') }}
|
||||||
</AButton>
|
</AButton>
|
||||||
</APopconfirm>
|
</APopconfirm>
|
||||||
|
<ADivider type="vertical" />
|
||||||
<APopconfirm
|
<APopconfirm
|
||||||
v-if="params.trash"
|
v-if="params.trash"
|
||||||
:cancel-text="$gettext('No')"
|
:cancel-text="$gettext('No')"
|
||||||
|
@ -601,8 +589,14 @@ const paginationSize = computed(() => {
|
||||||
.ant-table-scroll {
|
.ant-table-scroll {
|
||||||
.ant-table-body {
|
.ant-table-body {
|
||||||
overflow-x: auto !important;
|
overflow-x: auto !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.std-table {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
overflow-y: hidden !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
@ -624,6 +618,12 @@ const paginationSize = computed(() => {
|
||||||
:deep(.ant-form-inline .ant-form-item) {
|
:deep(.ant-form-inline .ant-form-item) {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-divider {
|
||||||
|
&:last-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
|
|
||||||
|
export function getPithyColumns(columns: Column[]) {
|
||||||
|
return columns.filter(c => {
|
||||||
|
return c.pithy === true && !c.hiddenInTable
|
||||||
|
})
|
||||||
|
}
|
|
@ -135,6 +135,7 @@ async function save() {
|
||||||
content: configText.value,
|
content: configText.value,
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
site_category_id: data.value.site_category_id,
|
site_category_id: data.value.site_category_id,
|
||||||
|
sync_node_ids: data.value.sync_node_ids,
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
handle_response(r)
|
handle_response(r)
|
||||||
router.push({
|
router.push({
|
||||||
|
|
|
@ -1,107 +1,17 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Ref } from 'vue'
|
|
||||||
import domain from '@/api/domain'
|
|
||||||
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
|
||||||
import { Modal, notification } from 'ant-design-vue'
|
|
||||||
|
|
||||||
const node_map = ref({})
|
const node_map = ref({})
|
||||||
const target = ref([])
|
const target = ref([])
|
||||||
const overwrite = ref(false)
|
|
||||||
const enabled = ref(false)
|
|
||||||
const name = inject('name') as Ref<string>
|
|
||||||
const [modal, ContextHolder] = Modal.useModal()
|
|
||||||
function deploy() {
|
|
||||||
modal.confirm({
|
|
||||||
title: () => $ngettext('Do you want to deploy this file to remote server?', 'Do you want to deploy this file to remote servers?', target.value.length),
|
|
||||||
mask: false,
|
|
||||||
centered: true,
|
|
||||||
okText: $gettext('OK'),
|
|
||||||
cancelText: $gettext('Cancel'),
|
|
||||||
onOk() {
|
|
||||||
target.value.forEach(id => {
|
|
||||||
const node_name = node_map.value[id]
|
|
||||||
|
|
||||||
// get source content
|
|
||||||
domain.get(name.value).then(r => {
|
|
||||||
domain.save(name.value, {
|
|
||||||
name: name.value,
|
|
||||||
content: r.config,
|
|
||||||
overwrite: overwrite.value,
|
|
||||||
|
|
||||||
}, { headers: { 'X-Node-ID': id } }).then(async () => {
|
|
||||||
notification.success({
|
|
||||||
message: $gettext('Deploy successfully'),
|
|
||||||
description:
|
|
||||||
$gettext('Deploy %{conf_name} to %{node_name} successfully', { conf_name: name.value, node_name }),
|
|
||||||
})
|
|
||||||
if (enabled.value) {
|
|
||||||
domain.enable(name.value, { headers: { 'X-Node-ID': id } }).then(() => {
|
|
||||||
notification.success({
|
|
||||||
message: $gettext('Enable successfully'),
|
|
||||||
description:
|
|
||||||
$gettext('Enable %{conf_name} in %{node_name} successfully', { conf_name: name.value, node_name }),
|
|
||||||
})
|
|
||||||
}).catch(e => {
|
|
||||||
notification.error({
|
|
||||||
message: $gettext('Enable %{conf_name} in %{node_name} failed', {
|
|
||||||
conf_name: name.value,
|
|
||||||
node_name,
|
|
||||||
}),
|
|
||||||
description: $gettext(e?.message ?? 'Server error'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch(e => {
|
|
||||||
notification.error({
|
|
||||||
message: $gettext('Deploy %{conf_name} to %{node_name} failed', {
|
|
||||||
conf_name: name.value,
|
|
||||||
node_name,
|
|
||||||
}),
|
|
||||||
description: $gettext(e?.message ?? 'Server error'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<NodeSelector
|
||||||
<ContextHolder />
|
v-model:target="target"
|
||||||
<NodeSelector
|
v-model:map="node_map"
|
||||||
v-model:target="target"
|
class="mb-4"
|
||||||
v-model:map="node_map"
|
hidden-local
|
||||||
hidden-local
|
/>
|
||||||
/>
|
|
||||||
<div class="node-deploy-control">
|
|
||||||
<ACheckbox v-model:checked="enabled">
|
|
||||||
{{ $gettext('Enable') }}
|
|
||||||
</ACheckbox>
|
|
||||||
<div class="overwrite">
|
|
||||||
<ACheckbox v-model:checked="overwrite">
|
|
||||||
{{ $gettext('Overwrite') }}
|
|
||||||
</ACheckbox>
|
|
||||||
<ATooltip placement="bottom">
|
|
||||||
<template #title>
|
|
||||||
{{ $gettext('Overwrite exist file') }}
|
|
||||||
</template>
|
|
||||||
<InfoCircleOutlined />
|
|
||||||
</ATooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AButton
|
|
||||||
:disabled="target.length === 0"
|
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
@click="deploy"
|
|
||||||
>
|
|
||||||
{{ $gettext('Deploy') }}
|
|
||||||
</AButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
|
|
|
@ -6,11 +6,12 @@ import type { Ref } from 'vue'
|
||||||
import domain from '@/api/domain'
|
import domain from '@/api/domain'
|
||||||
import site_category from '@/api/site_category'
|
import site_category from '@/api/site_category'
|
||||||
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
||||||
|
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
||||||
import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
|
import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
|
||||||
import { formatDateTime } from '@/lib/helper'
|
import { formatDateTime } from '@/lib/helper'
|
||||||
import { useSettingsStore } from '@/pinia'
|
import { useSettingsStore } from '@/pinia'
|
||||||
import Deploy from '@/views/site/components/Deploy.vue'
|
|
||||||
import siteCategoryColumns from '@/views/site/site_category/columns'
|
import siteCategoryColumns from '@/views/site/site_category/columns'
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
@ -71,6 +72,7 @@ function on_change_enabled(checked: CheckedType) {
|
||||||
<ACollapse
|
<ACollapse
|
||||||
v-model:active-key="active_key"
|
v-model:active-key="active_key"
|
||||||
ghost
|
ghost
|
||||||
|
collapsible="header"
|
||||||
>
|
>
|
||||||
<ACollapsePanel
|
<ACollapsePanel
|
||||||
key="1"
|
key="1"
|
||||||
|
@ -103,9 +105,33 @@ function on_change_enabled(checked: CheckedType) {
|
||||||
<ACollapsePanel
|
<ACollapsePanel
|
||||||
v-if="!settings.is_remote"
|
v-if="!settings.is_remote"
|
||||||
key="2"
|
key="2"
|
||||||
:header="$gettext('Deploy')"
|
|
||||||
>
|
>
|
||||||
<Deploy />
|
<template #header>
|
||||||
|
{{ $gettext('Synchronization') }}
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<APopover placement="bottomRight" :title="$gettext('Sync strategy')">
|
||||||
|
<template #content>
|
||||||
|
<div class="max-w-200px mb-2">
|
||||||
|
{{ $gettext('When you enable/disable, delete, or save this site, '
|
||||||
|
+ 'the nodes set in the site category and the nodes selected below will be synchronized.') }}
|
||||||
|
</div>
|
||||||
|
<div class="max-w-200px">
|
||||||
|
{{ $gettext('Note, if the configuration file include other configurations or certificates, '
|
||||||
|
+ 'please synchronize them to the remote nodes in advance.') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="text-trueGray-600">
|
||||||
|
<InfoCircleOutlined class="mr-1" />
|
||||||
|
{{ $gettext('Sync strategy') }}
|
||||||
|
</div>
|
||||||
|
</APopover>
|
||||||
|
</template>
|
||||||
|
<NodeSelector
|
||||||
|
v-model:target="data.sync_node_ids"
|
||||||
|
class="mb-4"
|
||||||
|
hidden-local
|
||||||
|
/>
|
||||||
</ACollapsePanel>
|
</ACollapsePanel>
|
||||||
<ACollapsePanel
|
<ACollapsePanel
|
||||||
key="3"
|
key="3"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
|
import type { Site } from '@/api/domain'
|
||||||
import type { SiteCategory } from '@/api/site_category'
|
import type { SiteCategory } from '@/api/site_category'
|
||||||
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
import domain from '@/api/domain'
|
import domain from '@/api/domain'
|
||||||
import site_category from '@/api/site_category'
|
import site_category from '@/api/site_category'
|
||||||
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
||||||
|
@ -7,6 +9,7 @@ import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue'
|
import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue'
|
||||||
import columns from '@/views/site/site_list/columns'
|
import columns from '@/views/site/site_list/columns'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import StdBatchEdit from '../../../components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -77,6 +80,17 @@ function handle_click_duplicate(name: string) {
|
||||||
show_duplicator.value = true
|
show_duplicator.value = true
|
||||||
target.value = name
|
target.value = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
|
||||||
|
|
||||||
|
async function handleClickBatchEdit(batchColumns: Column[], selectedRowKeys: string[], selectedRows: Site[]) {
|
||||||
|
stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys, selectedRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBatchUpdated() {
|
||||||
|
table.value?.get_list()
|
||||||
|
table.value?.resetSelection()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -101,9 +115,9 @@ function handle_click_duplicate(name: string) {
|
||||||
@click-edit="(r: string) => router.push({
|
@click-edit="(r: string) => router.push({
|
||||||
path: `/sites/${r}`,
|
path: `/sites/${r}`,
|
||||||
})"
|
})"
|
||||||
|
@click-batch-modify="handleClickBatchEdit"
|
||||||
>
|
>
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<ADivider type="vertical" />
|
|
||||||
<AButton
|
<AButton
|
||||||
v-if="record.enabled"
|
v-if="record.enabled"
|
||||||
type="link"
|
type="link"
|
||||||
|
@ -146,6 +160,12 @@ function handle_click_duplicate(name: string) {
|
||||||
</APopconfirm>
|
</APopconfirm>
|
||||||
</template>
|
</template>
|
||||||
</StdTable>
|
</StdTable>
|
||||||
|
<StdBatchEdit
|
||||||
|
ref="stdBatchEditRef"
|
||||||
|
:api="domain"
|
||||||
|
:columns
|
||||||
|
@save="handleBatchUpdated"
|
||||||
|
/>
|
||||||
<SiteDuplicate
|
<SiteDuplicate
|
||||||
v-model:visible="show_duplicator"
|
v-model:visible="show_duplicator"
|
||||||
:name="target"
|
:name="target"
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import type { Column, JSXElements } from '@/components/StdDesign/types'
|
import type { Column, JSXElements } from '@/components/StdDesign/types'
|
||||||
|
import site_category from '@/api/site_category'
|
||||||
import {
|
import {
|
||||||
actualValueRender,
|
actualValueRender,
|
||||||
type CustomRenderProps,
|
type CustomRenderProps,
|
||||||
datetime,
|
datetime,
|
||||||
} from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
|
} from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
|
||||||
import { input, select } from '@/components/StdDesign/StdDataEntry'
|
import { input, select, selector } from '@/components/StdDesign/StdDataEntry'
|
||||||
|
import siteCategoryColumns from '@/views/site/site_category/columns'
|
||||||
import { Badge } from 'ant-design-vue'
|
import { Badge } from 'ant-design-vue'
|
||||||
|
|
||||||
const columns: Column[] = [{
|
const columns: Column[] = [{
|
||||||
|
@ -20,8 +22,18 @@ const columns: Column[] = [{
|
||||||
title: () => $gettext('Category'),
|
title: () => $gettext('Category'),
|
||||||
dataIndex: 'site_category_id',
|
dataIndex: 'site_category_id',
|
||||||
customRender: actualValueRender('site_category.name'),
|
customRender: actualValueRender('site_category.name'),
|
||||||
|
edit: {
|
||||||
|
type: selector,
|
||||||
|
selector: {
|
||||||
|
api: site_category,
|
||||||
|
columns: siteCategoryColumns,
|
||||||
|
recordValueIndex: 'name',
|
||||||
|
selectionType: 'radio',
|
||||||
|
},
|
||||||
|
},
|
||||||
sorter: true,
|
sorter: true,
|
||||||
pithy: true,
|
pithy: true,
|
||||||
|
batch: true,
|
||||||
}, {
|
}, {
|
||||||
title: () => $gettext('Status'),
|
title: () => $gettext('Status'),
|
||||||
dataIndex: 'enabled',
|
dataIndex: 'enabled',
|
||||||
|
|
|
@ -135,7 +135,6 @@ function handleAddStream() {
|
||||||
})"
|
})"
|
||||||
>
|
>
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<ADivider type="vertical" />
|
|
||||||
<AButton
|
<AButton
|
||||||
v-if="record.enabled"
|
v-if="record.enabled"
|
||||||
type="link"
|
type="link"
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/0xJacky/Nginx-UI/model"
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
"github.com/uozi-tech/cosy/settings"
|
"github.com/0xJacky/Nginx-UI/settings"
|
||||||
|
cSettings "github.com/uozi-tech/cosy/settings"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gen"
|
"gorm.io/gen"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
@ -39,8 +40,8 @@ func main() {
|
||||||
flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file")
|
flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
settings.Init(confPath)
|
cSettings.Init(confPath)
|
||||||
dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DataBaseSettings.Name))
|
dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DatabaseSettings.Name))
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
|
|
|
@ -2,8 +2,9 @@ package model
|
||||||
|
|
||||||
type Site struct {
|
type Site struct {
|
||||||
Model
|
Model
|
||||||
Path string `json:"path"`
|
Path string `json:"path" gorm:"uniqueIndex"`
|
||||||
Advanced bool `json:"advanced"`
|
Advanced bool `json:"advanced"`
|
||||||
SiteCategoryID uint64 `json:"site_category_id"`
|
SiteCategoryID uint64 `json:"site_category_id"`
|
||||||
SiteCategory *SiteCategory `json:"site_category,omitempty"`
|
SiteCategory *SiteCategory `json:"site_category,omitempty"`
|
||||||
|
SyncNodeIDs []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ func newSite(db *gorm.DB, opts ...gen.DOOption) site {
|
||||||
_site.Path = field.NewString(tableName, "path")
|
_site.Path = field.NewString(tableName, "path")
|
||||||
_site.Advanced = field.NewBool(tableName, "advanced")
|
_site.Advanced = field.NewBool(tableName, "advanced")
|
||||||
_site.SiteCategoryID = field.NewUint64(tableName, "site_category_id")
|
_site.SiteCategoryID = field.NewUint64(tableName, "site_category_id")
|
||||||
|
_site.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
|
||||||
_site.SiteCategory = siteBelongsToSiteCategory{
|
_site.SiteCategory = siteBelongsToSiteCategory{
|
||||||
db: db.Session(&gorm.Session{}),
|
db: db.Session(&gorm.Session{}),
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ type site struct {
|
||||||
Path field.String
|
Path field.String
|
||||||
Advanced field.Bool
|
Advanced field.Bool
|
||||||
SiteCategoryID field.Uint64
|
SiteCategoryID field.Uint64
|
||||||
|
SyncNodeIDs field.Field
|
||||||
SiteCategory siteBelongsToSiteCategory
|
SiteCategory siteBelongsToSiteCategory
|
||||||
|
|
||||||
fieldMap map[string]field.Expr
|
fieldMap map[string]field.Expr
|
||||||
|
@ -81,6 +83,7 @@ func (s *site) updateTableName(table string) *site {
|
||||||
s.Path = field.NewString(table, "path")
|
s.Path = field.NewString(table, "path")
|
||||||
s.Advanced = field.NewBool(table, "advanced")
|
s.Advanced = field.NewBool(table, "advanced")
|
||||||
s.SiteCategoryID = field.NewUint64(table, "site_category_id")
|
s.SiteCategoryID = field.NewUint64(table, "site_category_id")
|
||||||
|
s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
|
||||||
|
|
||||||
s.fillFieldMap()
|
s.fillFieldMap()
|
||||||
|
|
||||||
|
@ -97,7 +100,7 @@ func (s *site) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *site) fillFieldMap() {
|
func (s *site) fillFieldMap() {
|
||||||
s.fieldMap = make(map[string]field.Expr, 8)
|
s.fieldMap = make(map[string]field.Expr, 9)
|
||||||
s.fieldMap["id"] = s.ID
|
s.fieldMap["id"] = s.ID
|
||||||
s.fieldMap["created_at"] = s.CreatedAt
|
s.fieldMap["created_at"] = s.CreatedAt
|
||||||
s.fieldMap["updated_at"] = s.UpdatedAt
|
s.fieldMap["updated_at"] = s.UpdatedAt
|
||||||
|
@ -105,6 +108,7 @@ func (s *site) fillFieldMap() {
|
||||||
s.fieldMap["path"] = s.Path
|
s.fieldMap["path"] = s.Path
|
||||||
s.fieldMap["advanced"] = s.Advanced
|
s.fieldMap["advanced"] = s.Advanced
|
||||||
s.fieldMap["site_category_id"] = s.SiteCategoryID
|
s.fieldMap["site_category_id"] = s.SiteCategoryID
|
||||||
|
s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue