123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- <script setup lang="ts">
- import type { Config } from '@/api/config'
- import type { ChatComplicationMessage } from '@/api/openai'
- import type { Ref } from 'vue'
- import config from '@/api/config'
- import ngx from '@/api/ngx'
- import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
- import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
- import { ConfigHistory } from '@/components/ConfigHistory'
- import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
- import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
- import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
- import { formatDateTime } from '@/lib/helper'
- import { useSettingsStore } from '@/pinia'
- import ConfigName from '@/views/config/components/ConfigName.vue'
- import InspectConfig from '@/views/config/InspectConfig.vue'
- import { HistoryOutlined, InfoCircleOutlined } from '@ant-design/icons-vue'
- import { message } from 'ant-design-vue'
- import _ from 'lodash'
- const settings = useSettingsStore()
- const route = useRoute()
- const router = useRouter()
- // eslint-disable-next-line vue/require-typed-ref
- const refForm = ref()
- const origName = ref('')
- const addMode = computed(() => !route.params.name)
- const showHistory = ref(false)
- const basePath = computed(() => {
- if (route.query.basePath)
- return _.trim(route?.query?.basePath?.toString(), '/')
- else if (typeof route.params.name === 'object')
- return (route.params.name as string[]).slice(0, -1).join('/')
- else
- return ''
- })
- const data = ref({
- name: '',
- content: '',
- filepath: '',
- sync_node_ids: [] as number[],
- sync_overwrite: false,
- } as Config)
- const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
- const activeKey = ref(['basic', 'deploy', 'chatgpt'])
- const modifiedAt = ref('')
- const nginxConfigBase = ref('')
- const newPath = computed(() => {
- // 组合路径后解码显示
- const path = [nginxConfigBase.value, basePath.value, data.value.name]
- .filter(v => v)
- .join('/')
- return path
- })
- const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string)
- const breadcrumbs = useBreadcrumbs()
- async function init() {
- const { name } = route.params
- data.value.name = name?.[name?.length - 1] ?? ''
- origName.value = data.value.name
- if (!addMode.value) {
- config.get(relativePath.value).then(r => {
- data.value = r
- historyChatgptRecord.value = r.chatgpt_messages
- modifiedAt.value = r.modified_at
- const filteredPath = _.trimEnd(data.value.filepath
- .replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name)
- .split('/')
- .filter(v => v)
- // Build accumulated path to maintain original encoding state
- let accumulatedPath = ''
- const path = filteredPath.map((segment, index) => {
- // Decode for display
- const decodedSegment = decodeURIComponent(segment)
- // Accumulated path keeps original encoding state
- if (index === 0) {
- accumulatedPath = segment
- }
- else {
- accumulatedPath = `${accumulatedPath}/${segment}`
- }
- return {
- name: 'Manage Configs',
- translatedName: () => decodedSegment,
- path: '/config',
- query: {
- dir: accumulatedPath,
- },
- hasChildren: false,
- }
- })
- breadcrumbs.value = [{
- name: 'Dashboard',
- translatedName: () => $gettext('Dashboard'),
- path: '/dashboard',
- hasChildren: false,
- }, {
- name: 'Manage Configs',
- translatedName: () => $gettext('Manage Configs'),
- path: '/config',
- hasChildren: false,
- }, ...path, {
- name: 'Edit Config',
- translatedName: () => origName.value,
- hasChildren: false,
- }]
- })
- }
- else {
- data.value.content = ''
- historyChatgptRecord.value = []
- data.value.filepath = ''
- const pathSegments = basePath.value
- .split('/')
- .filter(v => v)
- // Build accumulated path
- let accumulatedPath = ''
- const path = pathSegments.map((segment, index) => {
- // Decode for display
- const decodedSegment = decodeURIComponent(segment)
- // Accumulated path keeps original encoding state
- if (index === 0) {
- accumulatedPath = segment
- }
- else {
- accumulatedPath = `${accumulatedPath}/${segment}`
- }
- return {
- name: 'Manage Configs',
- translatedName: () => decodedSegment,
- path: '/config',
- query: {
- dir: accumulatedPath,
- },
- hasChildren: false,
- }
- })
- breadcrumbs.value = [{
- name: 'Dashboard',
- translatedName: () => $gettext('Dashboard'),
- path: '/dashboard',
- hasChildren: false,
- }, {
- name: 'Manage Configs',
- translatedName: () => $gettext('Manage Configs'),
- path: '/config',
- hasChildren: false,
- }, ...path, {
- name: 'Add Config',
- translatedName: () => $gettext('Add Configuration'),
- hasChildren: false,
- }]
- }
- }
- onMounted(async () => {
- await config.get_base_path().then(r => {
- nginxConfigBase.value = r.base_path
- })
- await init()
- })
- function save() {
- refForm.value?.validate().then(() => {
- config.save(addMode.value ? undefined : relativePath.value, {
- name: addMode.value ? data.value.name : undefined,
- base_dir: addMode.value ? basePath.value : undefined,
- content: data.value.content,
- sync_node_ids: data.value.sync_node_ids,
- sync_overwrite: data.value.sync_overwrite,
- }).then(r => {
- data.value.content = r.content
- message.success($gettext('Saved successfully'))
- if (addMode.value) {
- router.push({
- path: `/config/${data.value.name}/edit`,
- query: {
- basePath: basePath.value,
- },
- })
- }
- else {
- data.value = r
- }
- })
- })
- }
- function formatCode() {
- ngx.format_code(data.value.content).then(r => {
- data.value.content = r.content
- message.success($gettext('Format successfully'))
- }).catch(r => {
- message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
- })
- }
- function goBack() {
- // Keep original path with encoding state
- const encodedPath = basePath.value || ''
- router.push({
- path: '/config',
- query: {
- dir: encodedPath || undefined,
- },
- })
- }
- function openHistory() {
- showHistory.value = true
- }
- </script>
- <template>
- <ARow :gutter="16">
- <ACol
- :xs="24"
- :sm="24"
- :md="18"
- >
- <ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
- <template #extra>
- <AButton
- v-if="!addMode && data.filepath"
- type="link"
- @click="openHistory"
- >
- <template #icon>
- <HistoryOutlined />
- </template>
- {{ $gettext('History') }}
- </AButton>
- </template>
- <InspectConfig
- v-show="!addMode"
- />
- <CodeEditor v-model:content="data.content" />
- <FooterToolBar>
- <ASpace>
- <AButton @click="goBack">
- {{ $gettext('Back') }}
- </AButton>
- <AButton @click="formatCode">
- {{ $gettext('Format Code') }}
- </AButton>
- <AButton
- type="primary"
- @click="save"
- >
- {{ $gettext('Save') }}
- </AButton>
- </ASpace>
- </FooterToolBar>
- </ACard>
- </ACol>
- <ACol
- :xs="24"
- :sm="24"
- :md="6"
- >
- <ACard class="col-right">
- <ACollapse
- v-model:active-key="activeKey"
- ghost
- >
- <ACollapsePanel
- key="basic"
- :header="$gettext('Basic')"
- >
- <AForm
- ref="refForm"
- layout="vertical"
- :model="data"
- :rules="{
- name: [
- { required: true, message: $gettext('Please input a filename') },
- { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
- ],
- }"
- >
- <AFormItem
- name="name"
- :label="$gettext('Name')"
- >
- <AInput v-if="addMode" v-model:value="data.name" />
- <ConfigName v-else :name="data.name" :dir="data.dir" />
- </AFormItem>
- <AFormItem
- v-if="!addMode"
- :label="$gettext('Path')"
- >
- {{ decodeURIComponent(data.filepath) }}
- </AFormItem>
- <AFormItem
- v-show="data.name !== origName"
- :label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
- required
- >
- {{ decodeURIComponent(newPath) }}
- </AFormItem>
- <AFormItem
- v-if="!addMode"
- :label="$gettext('Updated at')"
- >
- {{ formatDateTime(modifiedAt) }}
- </AFormItem>
- </AForm>
- </ACollapsePanel>
- <ACollapsePanel
- v-if="!settings.is_remote"
- key="deploy"
- :header="$gettext('Deploy')"
- >
- <NodeSelector
- v-model:target="data.sync_node_ids"
- hidden-local
- />
- <div class="node-deploy-control">
- <div class="overwrite">
- <ACheckbox v-model:checked="data.sync_overwrite">
- {{ $gettext('Overwrite') }}
- </ACheckbox>
- <ATooltip placement="bottom">
- <template #title>
- {{ $gettext('Overwrite exist file') }}
- </template>
- <InfoCircleOutlined />
- </ATooltip>
- </div>
- </div>
- </ACollapsePanel>
- <ACollapsePanel
- key="chatgpt"
- header="ChatGPT"
- >
- <ChatGPT
- v-model:history-messages="historyChatgptRecord"
- :content="data.content"
- :path="data.filepath"
- />
- </ACollapsePanel>
- </ACollapse>
- </ACard>
- </ACol>
- <ConfigHistory
- v-model:visible="showHistory"
- v-model:current-content="data.content"
- :filepath="data.filepath"
- />
- </ARow>
- </template>
- <style lang="less" scoped>
- .col-right {
- position: sticky;
- top: 78px;
- :deep(.ant-card-body) {
- max-height: 100vh;
- overflow-y: scroll;
- }
- }
- :deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
- padding: 0;
- }
- :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
- padding: 0 0 10px 0;
- }
- .overwrite {
- margin-right: 15px;
- span {
- color: #9b9b9b;
- }
- }
- .node-deploy-control {
- display: flex;
- justify-content: flex-end;
- margin-top: 10px;
- align-items: center;
- }
- </style>
|