feat: theme supports preview after setting (#502)

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/505/head
Ryan Wang 2022-03-10 23:46:20 +08:00 committed by GitHub
parent bb28b3ff2f
commit c7e872d812
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 341 additions and 169 deletions

View File

@ -222,6 +222,13 @@ export const asyncRouterMap = [
}
]
},
{
path: '/interface/themes/setting/visual',
name: 'ThemeVisualSetting',
hidden: true,
component: () => import('@/views/interface/ThemeVisualSetting'),
meta: { title: '主题设置', hiddenHeaderContent: false }
},
{
path: '*',
redirect: '/404',

View File

@ -17,6 +17,24 @@
<a-icon type="down" />
</a-button>
</a-dropdown>
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item :disabled="theme.current.activated" @click="handleActiveTheme">
<a-icon type="lock" />
启用
</a-menu-item>
<a-menu-item :disabled="!theme.current.activated" @click="handleRouteToThemeVisualSetting">
<a-icon type="eye" />
预览模式
</a-menu-item>
</a-menu>
</template>
<a-button icon="more">
更多
<a-icon type="down" />
</a-button>
</a-dropdown>
<a-button
:disabled="theme.current.activated"
icon="delete"
@ -26,135 +44,9 @@
删除
</a-button>
</template>
<a-spin :spinning="theme.loading">
<div v-if="theme.current.id" class="card-container">
<a-tabs defaultActiveKey="0" type="card">
<a-tab-pane :key="0" tab="关于">
<div v-if="theme.current.logo">
<a-avatar :alt="theme.current.name" :size="72" :src="theme.current.logo" shape="square" />
<a-divider />
</div>
<a-descriptions :column="1" layout="horizontal">
<a-descriptions-item label="作者">
<a class="text-inherit" :href="theme.current.author.website || '#'" target="_blank">
{{ theme.current.author.name }}
</a>
</a-descriptions-item>
<a-descriptions-item label="介绍">
{{ theme.current.description || '-' }}
</a-descriptions-item>
<a-descriptions-item label="官网">
<a class="text-inherit" :href="theme.current.website || '#'" target="_blank">
{{ theme.current.website || '-' }}
</a>
</a-descriptions-item>
<a-descriptions-item label="Git 仓库">
<a class="text-inherit" :href="theme.current.repo || '#'" target="_blank">
{{ theme.current.repo || '-' }}
</a>
</a-descriptions-item>
<a-descriptions-item label="主题标识">
{{ theme.current.id }}
</a-descriptions-item>
<a-descriptions-item label="当前版本">
{{ theme.current.version }}
</a-descriptions-item>
<a-descriptions-item label="存储位置">
{{ theme.current.themePath }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane v-for="(group, index) in theme.configurations" :key="index + 1" :tab="group.label">
<a-form
:wrapperCol="{
xl: { span: 8 },
lg: { span: 8 },
sm: { span: 12 },
xs: { span: 24 }
}"
layout="vertical"
>
<a-form-item v-for="(item, formItemIndex) in group.items" :key="formItemIndex" :label="item.label + ''">
<p v-if="item.description && item.description !== ''" slot="help" v-html="item.description"></p>
<a-input
v-if="item.type === 'TEXT'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
<a-input
v-else-if="item.type === 'TEXTAREA'"
v-model="theme.settings[item.name]"
:autoSize="{ minRows: 5 }"
:placeholder="item.placeholder"
type="textarea"
/>
<a-radio-group
v-else-if="item.type === 'RADIO'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
>
<a-radio v-for="(option, radioIndex) in item.options" :key="radioIndex" :value="option.value">
{{ option.label }}
</a-radio>
</a-radio-group>
<a-select
v-else-if="item.type === 'SELECT'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
>
<a-select-option v-for="option in item.options" :key="option.value" :value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<verte
v-else-if="item.type === 'COLOR'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
model="hex"
picker="square"
style="display: inline-block; height: 24px"
></verte>
<AttachmentInput
v-else-if="item.type === 'ATTACHMENT'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
<a-input-number
v-else-if="item.type === 'NUMBER'"
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
style="width: 100%"
/>
<a-switch
v-else-if="item.type === 'SWITCH'"
v-model="theme.settings[item.name]"
:defaultChecked="item.defaultValue"
/>
<a-input
v-else
v-model="theme.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
</a-form-item>
<a-form-item>
<ReactiveButton
:errored="theme.saveErrored"
:loading="theme.saving"
erroredText="保存失败"
loadedText="保存成功"
text="保存"
type="primary"
@callback="theme.saveErrored = false"
@click="handleSaveSettings"
></ReactiveButton>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
<ThemeSettingForm :theme="theme.current" />
</a-spin>
<ThemeDeleteConfirmModal
@ -172,11 +64,10 @@
</template>
<script>
// components
import Verte from 'verte'
import 'verte/dist/verte.css'
import { PageView } from '@/layouts'
import ThemeDeleteConfirmModal from './components/ThemeDeleteConfirmModal'
import ThemeLocalUpgradeModal from './components/ThemeLocalUpgradeModal'
import ThemeSettingForm from './components/ThemeSettingForm'
// utils
import apiClient from '@/utils/api-client'
@ -185,19 +76,15 @@ export default {
name: 'ThemeSetting',
components: {
PageView,
Verte,
ThemeDeleteConfirmModal,
ThemeLocalUpgradeModal
ThemeLocalUpgradeModal,
ThemeSettingForm
},
data() {
return {
theme: {
current: {},
settings: [],
configurations: [],
loading: false,
saving: false,
saveErrored: false
loading: false
},
themeDeleteModal: {
visible: false
@ -225,41 +112,10 @@ export default {
const { data } = await apiClient.theme.getActivatedTheme()
this.theme.current = data
}
await this.handleGetConfigurations()
await this.handleGetSettings()
} finally {
this.theme.loading = false
}
},
async handleGetConfigurations() {
try {
const { data } = await apiClient.theme.listConfigurations(this.theme.current.id)
this.theme.configurations = data
} catch (error) {
this.$log.error(error)
}
},
async handleGetSettings() {
try {
const { data } = await apiClient.theme.listSettings(this.theme.current.id)
this.theme.settings = data
} catch (error) {
this.$log.error(error)
}
},
async handleSaveSettings() {
try {
this.theme.saving = true
await apiClient.theme.saveSettings(this.theme.current.id, this.theme.settings)
} catch (error) {
this.$log.error(error)
this.theme.saveErrored = true
} finally {
setTimeout(() => {
this.theme.saving = false
}, 400)
}
},
onThemeDeleteSucceed() {
this.$router.replace({ name: 'ThemeList' })
},
@ -268,7 +124,7 @@ export default {
_this.$confirm({
title: '提示',
maskClosable: true,
content: '确定更新【' + _this.theme.current.name + '】主题',
content: '确定更新【' + _this.theme.current.name + '】主题',
async onOk() {
const hideLoading = _this.$message.loading('更新中...', 0)
try {
@ -282,6 +138,27 @@ export default {
}
}
})
},
handleRouteToThemeVisualSetting() {
this.$router.push({ name: 'ThemeVisualSetting', query: { themeId: this.theme.current.id } })
},
handleActiveTheme() {
const _this = this
_this.$confirm({
title: '提示',
maskClosable: true,
content: '确定启用【' + _this.theme.current.name + '】主题吗?',
async onOk() {
try {
await apiClient.theme.active(_this.theme.current.id)
_this.$message.success('启用成功!')
} catch (e) {
_this.$log.error('Failed active theme', e)
} finally {
await _this.handleGetTheme(_this.theme.current.id)
}
}
})
}
}
}

View File

@ -0,0 +1,76 @@
<template>
<a-row :gutter="0">
<a-col :md="6" :sm="24" class="h-screen" style="border-right: 1px solid #f2f2f2">
<a-spin :spinning="theme.loading" class="h-full">
<ThemeSettingForm :theme="theme.data" :wrapperCol="{ span: 24 }" @saved="onSettingsSaved" />
</a-spin>
</a-col>
<a-col :md="18" :sm="24" class="h-screen">
<iframe
id="themeViewIframe"
:src="options.blog_url"
frameborder="0"
height="100%"
scrolling="auto"
width="100%"
/>
</a-col>
</a-row>
</template>
<script>
// components
import ThemeSettingForm from './components/ThemeSettingForm'
import apiClient from '@/utils/api-client'
import { mapGetters } from 'vuex'
export default {
name: 'ThemeVisualSetting',
components: {
ThemeSettingForm
},
data() {
return {
theme: {
data: {},
loading: false
}
}
},
computed: {
...mapGetters(['options'])
},
beforeRouteEnter(to, from, next) {
// Get theme id from query
const themeId = to.query.themeId
next(async vm => {
await vm.handleGetTheme(themeId)
})
},
methods: {
async handleGetTheme(themeId) {
try {
this.theme.loading = true
const { data } = await apiClient.theme.get(themeId)
this.theme.data = data
} finally {
this.theme.loading = false
}
},
onSettingsSaved() {
document.getElementById('themeViewIframe').contentWindow.location.reload()
}
}
}
</script>
<style scoped>
::v-deep .ant-spin-container {
height: 100%;
}
::v-deep .ant-tabs-content {
height: 100%;
overflow: auto;
padding-bottom: 20px;
}
</style>

View File

@ -0,0 +1,212 @@
<template>
<div v-if="theme.id" class="card-container h-full">
<a-tabs class="h-full" defaultActiveKey="0" type="card">
<a-tab-pane :key="0" tab="关于">
<div v-if="theme.logo">
<a-avatar :alt="theme.name" :size="72" :src="theme.logo" shape="square" />
<a-divider />
</div>
<a-descriptions :column="1" layout="horizontal">
<a-descriptions-item label="作者">
<a :href="theme.author.website || '#'" class="text-inherit" target="_blank">
{{ theme.author.name }}
</a>
</a-descriptions-item>
<a-descriptions-item label="介绍">
{{ theme.description || '-' }}
</a-descriptions-item>
<a-descriptions-item label="官网">
<a :href="theme.website || '#'" class="text-inherit" target="_blank">
{{ theme.website || '-' }}
</a>
</a-descriptions-item>
<a-descriptions-item label="Git 仓库">
<a :href="theme.repo || '#'" class="text-inherit" target="_blank">
{{ theme.repo || '-' }}
</a>
</a-descriptions-item>
<a-descriptions-item label="主题标识">
{{ theme.id }}
</a-descriptions-item>
<a-descriptions-item label="当前版本">
{{ theme.version }}
</a-descriptions-item>
<a-descriptions-item label="存储位置">
{{ theme.themePath }}
</a-descriptions-item>
</a-descriptions>
</a-tab-pane>
<a-tab-pane v-for="(group, index) in form.configurations" :key="index + 1" :tab="group.label">
<a-form :wrapperCol="wrapperCol" layout="vertical">
<a-form-item v-for="(item, formItemIndex) in group.items" :key="formItemIndex" :label="item.label + ''">
<p v-if="item.description && item.description !== ''" slot="help" v-html="item.description"></p>
<a-input
v-if="item.type === 'TEXT'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
<a-input
v-else-if="item.type === 'TEXTAREA'"
v-model="form.settings[item.name]"
:autoSize="{ minRows: 5 }"
:placeholder="item.placeholder"
type="textarea"
/>
<a-radio-group
v-else-if="item.type === 'RADIO'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
>
<a-radio v-for="(option, radioIndex) in item.options" :key="radioIndex" :value="option.value">
{{ option.label }}
</a-radio>
</a-radio-group>
<a-select
v-else-if="item.type === 'SELECT'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
>
<a-select-option v-for="option in item.options" :key="option.value" :value="option.value">
{{ option.label }}
</a-select-option>
</a-select>
<verte
v-else-if="item.type === 'COLOR'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
model="hex"
picker="square"
style="display: inline-block; height: 24px"
></verte>
<AttachmentInput
v-else-if="item.type === 'ATTACHMENT'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
<a-input-number
v-else-if="item.type === 'NUMBER'"
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
style="width: 100%"
/>
<a-switch
v-else-if="item.type === 'SWITCH'"
v-model="form.settings[item.name]"
:defaultChecked="item.defaultValue"
/>
<a-input
v-else
v-model="form.settings[item.name]"
:defaultValue="item.defaultValue"
:placeholder="item.placeholder"
/>
</a-form-item>
<a-form-item>
<ReactiveButton
:errored="form.saveErrored"
:loading="form.saving"
erroredText="保存失败"
loadedText="保存成功"
text="保存"
type="primary"
@callback="handleSaveSettingsCallback"
@click="handleSaveSettings"
></ReactiveButton>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
// components
import Verte from 'verte'
import 'verte/dist/verte.css'
import apiClient from '@/utils/api-client'
export default {
name: 'ThemeSettingForm',
components: {
Verte
},
props: {
theme: {
type: Object,
default: () => {}
},
wrapperCol: {
type: Object,
default: () => {
return {
xl: { span: 8 },
lg: { span: 8 },
sm: { span: 12 },
xs: { span: 24 }
}
}
}
},
data() {
return {
form: {
settings: [],
configurations: [],
loading: false,
saving: false,
saveErrored: false
}
}
},
watch: {
theme(value) {
if (value) {
this.handleGetConfigurations()
}
}
},
methods: {
async handleGetConfigurations() {
try {
const { data } = await apiClient.theme.listConfigurations(this.theme.id)
this.form.configurations = data
await this.handleGetSettings()
} catch (error) {
this.$log.error(error)
}
},
async handleGetSettings() {
try {
const { data } = await apiClient.theme.listSettings(this.theme.id)
this.form.settings = data
} catch (error) {
this.$log.error(error)
}
},
async handleSaveSettings() {
try {
this.form.saving = true
await apiClient.theme.saveSettings(this.theme.id, this.form.settings)
} catch (error) {
this.$log.error(error)
this.form.saveErrored = true
} finally {
setTimeout(() => {
this.form.saving = false
}, 400)
}
},
handleSaveSettingsCallback() {
if (this.form.saveErrored) {
this.form.saveErrored = false
} else {
this.handleGetSettings()
this.$emit('saved')
}
}
}
}
</script>