refactor: photo management (#468)

Signed-off-by: Ryan Wang <i@ryanc.cc>
pull/473/head
Ryan Wang 2022-02-26 21:13:56 +08:00 committed by GitHub
parent 51842f2afa
commit ea955d49dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 560 additions and 251 deletions

View File

@ -23,7 +23,7 @@
"@codemirror/basic-setup": "^0.19.1", "@codemirror/basic-setup": "^0.19.1",
"@codemirror/lang-html": "^0.19.4", "@codemirror/lang-html": "^0.19.4",
"@codemirror/lang-java": "^0.19.1", "@codemirror/lang-java": "^0.19.1",
"@halo-dev/admin-api": "^1.0.0-alpha.49", "@halo-dev/admin-api": "^1.0.0-alpha.50",
"@halo-dev/editor": "^3.0.0-alpha.2", "@halo-dev/editor": "^3.0.0-alpha.2",
"ant-design-vue": "^1.7.8", "ant-design-vue": "^1.7.8",
"dayjs": "^1.10.7", "dayjs": "^1.10.7",

View File

@ -6,7 +6,7 @@ specifiers:
'@codemirror/basic-setup': ^0.19.1 '@codemirror/basic-setup': ^0.19.1
'@codemirror/lang-html': ^0.19.4 '@codemirror/lang-html': ^0.19.4
'@codemirror/lang-java': ^0.19.1 '@codemirror/lang-java': ^0.19.1
'@halo-dev/admin-api': ^1.0.0-alpha.49 '@halo-dev/admin-api': ^1.0.0-alpha.50
'@halo-dev/editor': ^3.0.0-alpha.2 '@halo-dev/editor': ^3.0.0-alpha.2
'@vue/cli-plugin-babel': ~5.0.1 '@vue/cli-plugin-babel': ~5.0.1
'@vue/cli-plugin-eslint': ~5.0.1 '@vue/cli-plugin-eslint': ~5.0.1
@ -51,7 +51,7 @@ dependencies:
'@codemirror/basic-setup': 0.19.1 '@codemirror/basic-setup': 0.19.1
'@codemirror/lang-html': 0.19.4 '@codemirror/lang-html': 0.19.4
'@codemirror/lang-java': 0.19.1 '@codemirror/lang-java': 0.19.1
'@halo-dev/admin-api': 1.0.0-alpha.49 '@halo-dev/admin-api': 1.0.0-alpha.50
'@halo-dev/editor': 3.0.0-alpha.2 '@halo-dev/editor': 3.0.0-alpha.2
ant-design-vue: 1.7.8_9065e7474e033a8e4b95615fc8e6c36c ant-design-vue: 1.7.8_9065e7474e033a8e4b95615fc8e6c36c
dayjs: 1.10.7 dayjs: 1.10.7
@ -1575,11 +1575,11 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@halo-dev/admin-api/1.0.0-alpha.49: /@halo-dev/admin-api/1.0.0-alpha.50:
resolution: {integrity: sha512-549LNmiVDyjLfg9uvVYYCCJmfge8+hrNDk4Q9M0BUT9XB9xeQcV0S06Tz2MKR/3IAZhq8h2V3g5gIqaY7N9e9A==} resolution: {integrity: sha512-/kfkBPRqnhMRCMhKWMWz+rboUAxGCM5Tcv13BaBKlG04wEY7ewgrXrFMvxfYd0luIik+knluwKj1OwWix+2ZhA==}
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
'@halo-dev/rest-api-client': 1.0.0-alpha.49 '@halo-dev/rest-api-client': 1.0.0-alpha.50
tslib: 2.3.1 tslib: 2.3.1
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@ -1614,18 +1614,18 @@ packages:
vue: 2.6.14 vue: 2.6.14
dev: false dev: false
/@halo-dev/logger/1.0.0-alpha.49: /@halo-dev/logger/1.0.0-alpha.50:
resolution: {integrity: sha512-gshHffq0ntrPjAHCwRt6J3NJYvQfnlZsRgbBjnFyI5TQOSB5uFZ2dcxRAc75wDLTRFi2Brpsdm//odRe7PWGpg==} resolution: {integrity: sha512-Wl+MA6LfdCWecvmOdQkNzpWpddnfv3LlsM2AdmH8t5tznK984EAp2Zs+JgsIKyqyNdaFg3H70qoyAMiZQyzUVA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
dependencies: dependencies:
tslib: 2.3.1 tslib: 2.3.1
dev: false dev: false
/@halo-dev/rest-api-client/1.0.0-alpha.49: /@halo-dev/rest-api-client/1.0.0-alpha.50:
resolution: {integrity: sha512-tv2y4EvfJt55iAuUASKjqVv3Qt6xhh4oTG/qO+vr+Z+DEcMos+iX/fpeZwHW5/LmR0wdercBncOEP1qgOX9L0g==} resolution: {integrity: sha512-aEmCQAgboJ4rsjtSz3bCM2I97198bnxXvVAzpljReIUBk+W3bY1MNrJsvnqwBGGQqfcmjYnOoX2IPs7STyriKg==}
engines: {node: '>=12'} engines: {node: '>=12'}
dependencies: dependencies:
'@halo-dev/logger': 1.0.0-alpha.49 '@halo-dev/logger': 1.0.0-alpha.50
axios: 0.24.0 axios: 0.24.0
form-data: 4.0.0 form-data: 4.0.0
js-base64: 3.7.2 js-base64: 3.7.2

View File

@ -807,7 +807,7 @@ body {
} }
// 附件图片样式 // 附件图片样式
.attachments-group { .attachments-group, .photos-group {
&-item { &-item {
padding: 0; padding: 0;
height: 130px; height: 130px;

View File

@ -8,14 +8,14 @@
<a-row :gutter="48"> <a-row :gutter="48">
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="关键词:"> <a-form-item label="关键词:">
<a-input v-model="list.queryParam.keyword" /> <a-input v-model="list.params.keyword" allowClear @keyup.enter="handleQuery" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :md="6" :sm="24"> <a-col :md="6" :sm="24">
<a-form-item label="分组:"> <a-form-item label="分组:">
<a-select v-model="list.queryParam.team" @change="handleQuery()"> <a-select v-model="list.params.team" allowClear @change="handleQuery()">
<a-select-option v-for="(item, index) in computedTeams" :key="index" :value="item" <a-select-option v-for="(item, index) in computedTeams" :key="index" :value="item">
>{{ item }} {{ item }}
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -32,46 +32,108 @@
</a-form> </a-form>
</div> </div>
<div class="mb-0 table-operator"> <div class="mb-0 table-operator">
<a-button icon="plus" type="primary" @click="form.visible = true">添加</a-button> <a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="single" @click="handleOpenForm({})"> </a-menu-item>
<a-menu-item key="batch" @click="attachmentSelectModal.visible = true"> 批量添加</a-menu-item>
</a-menu>
</template>
<a-button icon="plus" type="primary">
添加
<a-icon type="down" />
</a-button>
</a-dropdown>
<a-button v-show="list.selected.length" icon="check-circle" type="primary" @click="handleSelectAll">
全选
</a-button>
<a-button v-show="list.selected.length" icon="delete" type="danger" @click="handleDeletePhotoInBatch">
删除
</a-button>
<a-button v-show="list.selected.length" icon="delete" @click="handleOpenUpdateTeamForm">
更改分组
</a-button>
<a-button v-show="list.selected.length" icon="close" @click="list.selected = []"> </a-button>
</div> </div>
</a-card> </a-card>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-list <a-list
:dataSource="list.data" :dataSource="list.data"
:grid="{ gutter: 12, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }" :grid="{ gutter: 6, xs: 2, sm: 2, md: 4, lg: 6, xl: 6, xxl: 6 }"
:loading="list.loading" :loading="list.loading"
class="photos-group"
> >
<a-list-item :key="index" slot="renderItem" slot-scope="item, index"> <template #renderItem="item, index">
<a-card :bodyStyle="{ padding: 0 }" hoverable @click="handleOpenEditForm(item)"> <a-list-item
<div class="photo-thumb"> :key="index"
<img :src="item.thumbnail" loading="lazy" /> @click="handleItemClick(item)"
@mouseenter="$set(item, 'hover', true)"
@mouseleave="$set(item, 'hover', false)"
>
<div
:class="`${isItemSelect(item) ? 'border-blue-600' : 'border-slate-200'}`"
class="border border-solid"
>
<div class="photo-thumb photos-group-item">
<span
:style="`background-image:url(${encodeURI(item.thumbnail)})`"
class="photos-group-item-img"
loading="lazy"
/>
</div>
<a-card-meta class="p-2 cursor-pointer">
<template #description>
<a-tooltip :title="item.name">
<div class="truncate">
<span class="mr-1">{{ item.name }}</span>
<span v-if="item.team" class="text-gray-500 text-xs">#{{ item.team }}</span>
</div>
</a-tooltip>
</template>
</a-card-meta>
<a-icon
v-show="!isItemSelect(item) && item.hover"
:style="{ fontSize: '18px', color: 'rgb(37 99 235)' }"
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
theme="twoTone"
type="plus-circle"
@click.stop="handleSelect(item)"
/>
<a-icon
v-show="isItemSelect(item)"
:style="{ fontSize: '18px', color: 'rgb(37 99 235)' }"
class="absolute top-1 right-2 font-bold cursor-pointer transition-all"
theme="twoTone"
type="check-circle"
/>
</div> </div>
<a-card-meta class="p-3"> </a-list-item>
<ellipsis slot="description" :length="isMobile() ? 12 : 16" tooltip>{{ item.name }}</ellipsis> </template>
</a-card-meta>
</a-card>
</a-list-item>
</a-list> </a-list>
</a-col> </a-col>
</a-row> </a-row>
<div class="page-wrapper"> <div class="page-wrapper">
<a-pagination <a-pagination
:current="list.pagination.page" :current="pagination.page"
:defaultPageSize="list.pagination.size" :defaultPageSize="pagination.size"
:pageSizeOptions="['18', '36', '54', '72', '90', '108']" :pageSizeOptions="['18', '36', '54', '72', '90', '108']"
:total="list.pagination.total" :total="pagination.total"
showLessItems showLessItems
showSizeChanger showSizeChanger
@change="handlePaginationChange" @change="handlePageChange"
@showSizeChange="handlePaginationChange" @showSizeChange="handlePageSizeChange"
/> />
</div> </div>
<div style="position: fixed; bottom: 30px; right: 30px"> <div style="position: fixed; bottom: 30px; right: 30px">
<a-button icon="setting" shape="circle" size="large" type="primary" @click="optionFormVisible = true"></a-button> <a-button icon="setting" shape="circle" size="large" type="primary" @click="optionFormVisible = true"></a-button>
</div> </div>
<a-modal v-model="optionFormVisible" :afterClose="() => (optionFormVisible = false)" title="页面设置"> <a-modal v-model="optionFormVisible" :afterClose="() => (optionFormVisible = false)" title="页面设置">
<template slot="footer"> <template #footer>
<a-button key="submit" type="primary" @click="handleSaveOptions()"></a-button> <a-button key="submit" type="primary" @click="handleSaveOptions()"></a-button>
</template> </template>
<a-form layout="vertical"> <a-form layout="vertical">
@ -83,144 +145,92 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<a-drawer
:title="`图片${form.model.id ? '修改' : '添加'}`" <a-modal v-model="updateTeamForm.visible" title="更改分组">
:visible="form.visible" <a-form layout="vertical">
:width="isMobile() ? '100%' : '480'" <a-form-item label="分组名称:">
closable <a-auto-complete
destroyOnClose ref="teamInput"
@close="onDrawerClose" v-model="updateTeamForm.team"
> :dataSource="computedTeams"
<a-form-model ref="photoForm" :model="form.model" :rules="form.rules" layout="vertical"> allowClear
<a-form-model-item label="图片地址:" prop="url">
<div class="pb-2">
<img
:src="form.model.url || '/images/placeholder.jpg'"
class="w-full cursor-pointer"
style="border-radius: 4px"
@click="attachmentSelectModal.visible = true"
/>
</div>
<a-input v-model="form.model.url" placeholder="点击封面图选择图片,或者输入外部链接" />
</a-form-model-item>
<a-form-model-item label="缩略图地址:" prop="thumbnail">
<a-input v-model="form.model.thumbnail" />
</a-form-model-item>
<a-form-model-item label="图片名称:" prop="name">
<a-input v-model="form.model.name" />
</a-form-model-item>
<a-form-model-item label="拍摄日期:" prop="takeTime">
<a-date-picker
:defaultValue="takeTimeDefaultValue"
format="YYYY-MM-DD HH:mm:ss"
showTime
style="width: 100%" style="width: 100%"
@change="onTakeTimeChange"
@ok="onTakeTimeSelect"
/> />
</a-form-model-item> </a-form-item>
<a-form-model-item label="拍摄地点:" prop="location"> </a-form>
<a-input v-model="form.model.location" />
</a-form-model-item> <template #footer>
<a-form-model-item label="分组:" prop="team"> <ReactiveButton
<a-auto-complete v-model="form.model.team" :dataSource="computedTeams" allowClear style="width: 100%" /> :errored="updateTeamForm.saveErrored"
</a-form-model-item> :loading="updateTeamForm.saving"
<a-form-model-item label="描述:" prop="description"> erroredText="更改失败"
<a-input v-model="form.model.description" :autoSize="{ minRows: 5 }" type="textarea" /> loadedText="更改成功"
</a-form-model-item> text="确定"
</a-form-model> @callback="handleUpdateTeamInBatchCallback"
<a-divider class="divider-transparent" /> @click="handleUpdateTeamInBatch"
<div class="bottom-control"> ></ReactiveButton>
<a-space> <a-button @click="updateTeamForm.visible = false">关闭</a-button>
<ReactiveButton </template>
:errored="form.saveErrored" </a-modal>
:erroredText="`${form.model.id ? '修改' : '添加'}失败`"
:loadedText="`${form.model.id ? '修改' : '添加'}成功`" <PhotoFormModal :photo="list.current" :teams="computedTeams" :visible.sync="formVisible" @succeed="onSaveSucceed">
:loading="form.saving" <template #extraFooter>
:text="`${form.model.id ? '修改' : '添加'}`" <a-button :disabled="selectPreviousButtonDisabled" @click="handleSelectPrevious"></a-button>
@callback="handleCreateOrUpdateCallback" <a-button :disabled="selectNextButtonDisabled" @click="handleSelectNext"></a-button>
@click="handleCreateOrUpdate" </template>
></ReactiveButton> </PhotoFormModal>
<a-popconfirm
v-if="form.model.id" <AttachmentSelectModal :visible.sync="attachmentSelectModal.visible" @confirm="handleAttachmentSelected" />
cancelText="取消"
okText="确定"
title="你确定要删除该图片?"
@confirm="handleDelete"
>
<ReactiveButton
:errored="form.deleteErrored"
:loading="form.deleting"
erroredText="删除失败"
loadedText="删除成功"
text="删除"
type="danger"
@callback="handleDeleteCallback"
@click="() => {}"
></ReactiveButton>
</a-popconfirm>
</a-space>
</div>
<AttachmentSelectModal
:multiSelect="false"
:visible.sync="attachmentSelectModal.visible"
@confirm="handleAttachmentSelected"
/>
</a-drawer>
</page-view> </page-view>
</template> </template>
<script> <script>
// components
import { PageView } from '@/layouts' import { PageView } from '@/layouts'
import PhotoFormModal from './components/PhotoFormModal'
import { mapActions } from 'vuex' import { mapActions } from 'vuex'
import { mixin, mixinDevice } from '@/mixins/mixin.js' import { mixin, mixinDevice } from '@/mixins/mixin.js'
import apiClient from '@/utils/api-client' import apiClient from '@/utils/api-client'
import { datetimeFormat } from '@/utils/datetime'
export default { export default {
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
components: { PageView }, components: { PageView, PhotoFormModal },
data() { data() {
return { return {
list: { list: {
data: [], data: [],
loading: false, loading: false,
pagination: { params: {
page: 1,
size: 18,
sort: null,
total: 1
},
queryParam: {
page: 0, page: 0,
size: 18, size: 18,
sort: null, sort: ['createTime,desc', 'id,asc'],
keyword: null, keyword: null,
team: null team: undefined
}
},
form: {
model: {},
visible: false,
rules: {
url: [{ required: true, message: '* 图片地址不能为空', trigger: ['change'] }],
thumbnail: [{ required: true, message: '* 缩略图地址不能为空', trigger: ['change'] }],
name: [{ required: true, message: '* 图片名称不能为空', trigger: ['change'] }]
}, },
saving: false, total: 0,
saveErrored: false, hasPrevious: false,
deleting: false, hasNext: false,
deleteErrored: false selected: [],
current: {}
}, },
attachmentSelectModal: { attachmentSelectModal: {
visible: false visible: false
}, },
optionFormVisible: false, updateTeamForm: {
team: undefined,
visible: false,
saving: false,
saveErrored: false
},
formVisible: false,
teams: [], teams: [],
options: [] options: [],
optionFormVisible: false
} }
}, },
created() { created() {
@ -229,140 +239,214 @@ export default {
this.handleListOptions() this.handleListOptions()
}, },
computed: { computed: {
takeTimeDefaultValue() { pagination() {
if (this.form.model.takeTime) { return {
const date = new Date(this.form.model.takeTime) page: this.list.params.page + 1,
return datetimeFormat(date, 'YYYY-MM-DD HH:mm:ss') size: this.list.params.size,
total: this.list.total
} }
return datetimeFormat(new Date(), 'YYYY-MM-DD HH:mm:ss')
}, },
computedTeams() { computedTeams() {
return this.teams.filter(item => { return this.teams.filter(item => {
return item !== '' return item !== ''
}) })
},
isItemSelect() {
return function (photo) {
return this.list.selected.findIndex(item => item.id === photo.id) > -1
}
},
selectPreviousButtonDisabled() {
const index = this.list.data.findIndex(photo => photo.id === this.list.current.id)
return index === 0 && !this.list.hasPrevious
},
selectNextButtonDisabled() {
const index = this.list.data.findIndex(photo => photo.id === this.list.current.id)
return index === this.list.data.length - 1 && !this.list.hasNext
} }
}, },
methods: { methods: {
...mapActions(['refreshOptionsCache']), ...mapActions(['refreshOptionsCache']),
handleListPhotos() {
this.list.loading = true async handleListPhotos() {
this.list.queryParam.page = this.list.pagination.page - 1 try {
this.list.queryParam.size = this.list.pagination.size this.list.loading = true
this.list.queryParam.sort = this.list.pagination.sort
apiClient.photo const response = await apiClient.photo.list(this.list.params)
.list(this.list.queryParam)
.then(response => { this.list.data = response.data.content
this.list.data = response.data.content this.list.total = response.data.total
this.list.pagination.total = response.data.total this.list.hasPrevious = response.data.hasPrevious
}) this.list.hasNext = response.data.hasNext
.finally(() => { } catch (e) {
this.list.loading = false this.$log.error('Failed to get photos', e)
}) } finally {
}, this.list.loading = false
handleQuery() { }
this.handlePaginationChange(1, this.list.pagination.size)
},
handleListOptions() {
apiClient.option.list().then(response => {
this.options = response.data
})
}, },
handleListPhotoTeams() { handleListPhotoTeams() {
apiClient.photo.listTeams().then(response => { apiClient.photo.listTeams().then(response => {
this.teams = response.data this.teams = response.data
}) })
}, },
handleCreateOrUpdate() {
/**
* Handle page change
*/
handlePageChange(page = 1) {
this.list.params.page = page - 1
this.handleListPhotos()
},
/**
* Handle page size change
*/
handlePageSizeChange(current, size) {
this.$log.debug(`Current: ${current}, PageSize: ${size}`)
this.list.params.page = 0
this.list.params.size = size
this.handleListPhotos()
},
handleQuery() {
this.handlePageChange(1)
},
handleResetParam() {
this.list.params.keyword = undefined
this.list.params.team = undefined
this.handlePageChange(1)
this.handleListPhotoTeams()
},
handleItemClick(photo) {
if (this.list.selected.length <= 0) {
this.handleOpenForm(photo)
return
}
this.isItemSelect(photo) ? this.handleUnselect(photo) : this.handleSelect(photo)
},
handleOpenForm(photo) {
this.list.current = photo
this.formVisible = true
},
handleSelect(photo) {
this.list.selected = [...this.list.selected, photo]
},
handleUnselect(photo) {
this.list.selected = this.list.selected.filter(item => item.id !== photo.id)
},
handleSelectAll() {
this.list.selected = this.list.data
},
async handleAttachmentSelected({ raw }) {
if (!raw.length) {
return
}
const photosToStage = raw.map(attachment => {
return {
name: attachment.name,
url: attachment.path,
thumbnail: attachment.thumbPath
}
})
try {
await apiClient.photo.createInBatch(photosToStage)
this.$message.success('添加成功')
} catch (e) {
this.$log.error('Failed to create photos in batch', e)
} finally {
await this.handleListPhotos()
this.handleListPhotoTeams()
}
},
async handleDeletePhotoInBatch() {
if (this.list.selected.length <= 0) {
this.$message.warn('你还未选择任何图片,请至少选择一个!')
return
}
const _this = this const _this = this
_this.$refs.photoForm.validate(valid => { this.$confirm({
if (valid) { title: '确定要批量删除选中的图片吗?',
_this.form.saving = true content: '一旦删除不可恢复,请谨慎操作',
if (_this.form.model.id) { async onOk() {
apiClient.photo try {
.update(_this.form.model.id, _this.form.model) const photoIds = _this.list.selected.map(photo => photo.id)
.catch(() => {
_this.form.saveErrored = true await apiClient.photo.deleteInBatch(photoIds)
})
.finally(() => { _this.list.selected = []
setTimeout(() => { _this.$message.success('删除成功')
_this.form.saving = false } catch (e) {
}, 400) _this.$log.error('Failed to delete selected photos', e)
}) } finally {
} else { await _this.handleListPhotos()
apiClient.photo _this.handleListPhotoTeams()
.create(_this.form.model)
.catch(() => {
_this.form.saveErrored = true
})
.finally(() => {
setTimeout(() => {
_this.form.saving = false
}, 400)
})
} }
} }
}) })
}, },
handleCreateOrUpdateCallback() {
if (this.form.saveErrored) { async handleUpdateTeamInBatch() {
this.form.saveErrored = false const photosToStage = this.list.selected.map(photo => {
return {
...photo,
team: this.updateTeamForm.team
}
})
try {
this.updateTeamForm.saving = true
await apiClient.photo.updateInBatch(photosToStage)
this.$message.success('更改成功')
} catch (e) {
this.updateTeamForm.saveErrored = true
this.$log.error('Failed to change team in batch', e)
} finally {
setTimeout(() => {
this.updateTeamForm.saving = false
}, 400)
}
},
handleUpdateTeamInBatchCallback() {
if (this.updateTeamForm.saveErrored) {
this.updateTeamForm.saveErrored = false
} else { } else {
this.form.model = {} this.updateTeamForm.visible = false
this.form.visible = false this.updateTeamForm.team = undefined
this.list.selected = []
this.handleListPhotos() this.handleListPhotos()
this.handleListPhotoTeams()
} }
}, },
handleOpenEditForm(photo) {
this.form.model = photo handleOpenUpdateTeamForm() {
this.form.visible = true this.updateTeamForm.visible = true
this.$nextTick(() => {
this.$refs.teamInput.focus()
})
}, },
handlePaginationChange(page, size) {
this.$log.debug(`Current: ${page}, PageSize: ${size}`) async onSaveSucceed(photo) {
this.list.pagination.page = page await this.handleListPhotos()
this.list.pagination.size = size this.list.current = photo
this.handleListPhotos()
},
handleDelete() {
this.form.deleting = true
apiClient.photo
.delete(this.form.model.id)
.catch(() => {
this.form.deleteErrored = true
})
.finally(() => {
setTimeout(() => {
this.form.deleting = false
}, 400)
})
},
handleDeleteCallback() {
if (this.form.deleteErrored) {
this.form.deleteErrored = false
} else {
this.form.model = {}
this.form.visible = false
this.handleListPhotos()
this.handleListPhotoTeams()
}
},
handleAttachmentSelected({ raw }) {
if (raw.length) {
this.form.model.url = encodeURI(raw[0].path)
this.form.model.thumbnail = encodeURI(raw[0].thumbPath)
}
this.attachmentSelectModal.visible = false
},
handleResetParam() {
this.list.queryParam.keyword = null
this.list.queryParam.team = null
this.handlePaginationChange(1, this.list.pagination.size)
this.handleListPhotoTeams() this.handleListPhotoTeams()
}, },
onDrawerClose() {
this.form.visible = false handleListOptions() {
this.form.model = {} apiClient.option.list().then(response => {
this.options = response.data
})
}, },
handleSaveOptions() { handleSaveOptions() {
apiClient.option apiClient.option
.save(this.options) .save(this.options)
@ -375,11 +459,39 @@ export default {
this.refreshOptionsCache() this.refreshOptionsCache()
}) })
}, },
onTakeTimeChange(value) {
this.form.model.takeTime = value.valueOf() /**
* Select previous photo
*/
async handleSelectPrevious() {
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
if (index > 0) {
this.list.current = this.list.data[index - 1]
return
}
if (index === 0 && this.list.hasPrevious) {
this.list.params.page--
await this.handleListPhotos()
this.list.current = this.list.data[this.list.data.length - 1]
}
}, },
onTakeTimeSelect(value) {
this.form.model.takeTime = value.valueOf() /**
* Select next photo
*/
async handleSelectNext() {
const index = this.list.data.findIndex(item => item.id === this.list.current.id)
if (index < this.list.data.length - 1) {
this.list.current = this.list.data[index + 1]
return
}
if (index === this.list.data.length - 1 && this.list.hasNext) {
this.list.params.page++
await this.handleListPhotos()
this.list.current = this.list.data[0]
}
} }
} }
} }

View File

@ -0,0 +1,197 @@
<template>
<a-modal v-model="modalVisible" :afterClose="onClosed" :maskClosable="false" :width="680" destroyOnClose>
<template #title>
{{ form.model.id ? '修改' : '添加' }}图片
<a-icon v-if="loading" type="loading" />
</template>
<a-form-model
ref="photoForm"
:label-col="{ span: 4 }"
:model="form.model"
:rules="form.rules"
:wrapper-col="{ span: 20 }"
labelAlign="left"
>
<a-form-model-item label="图片地址:" prop="url">
<a-space direction="vertical">
<img
:src="form.model.url || '/images/placeholder.jpg'"
class="w-1/2 cursor-pointer"
style="border-radius: 4px"
@click="attachmentSelectModal.visible = true"
/>
<a-input v-model="form.model.url" allow-clear placeholder="点击封面图选择图片,或者输入外部链接" />
</a-space>
</a-form-model-item>
<a-form-model-item label="缩略图地址:" prop="thumbnail">
<a-input v-model="form.model.thumbnail" />
</a-form-model-item>
<a-form-model-item label="图片名称:" prop="name">
<a-input v-model="form.model.name" />
</a-form-model-item>
<a-form-model-item label="拍摄日期:" prop="takeTime">
<a-date-picker
:defaultValue="takeTimeDefaultValue"
format="YYYY-MM-DD HH:mm:ss"
showTime
style="width: 100%"
@change="onTakeTimeChange"
@ok="onTakeTimeChange"
/>
</a-form-model-item>
<a-form-model-item label="拍摄地点:" prop="location">
<a-input v-model="form.model.location" />
</a-form-model-item>
<a-form-model-item label="分组:" prop="team">
<a-auto-complete v-model="form.model.team" :dataSource="teams" allowClear style="width: 100%" />
</a-form-model-item>
<a-form-model-item label="描述:" prop="description">
<a-input v-model="form.model.description" />
</a-form-model-item>
</a-form-model>
<template #footer>
<slot name="extraFooter" />
<ReactiveButton
:errored="form.saveErrored"
:loading="form.saving"
erroredText="保存失败"
loadedText="保存成功"
text="保存"
@callback="handleSaveCallback"
@click="handleSave"
></ReactiveButton>
<a-button :disabled="loading" @click="modalVisible = false"> 关闭</a-button>
</template>
<AttachmentSelectModal
:multiSelect="false"
:visible.sync="attachmentSelectModal.visible"
@confirm="handleAttachmentSelected"
/>
</a-modal>
</template>
<script>
import { datetimeFormat } from '@/utils/datetime'
import apiClient from '@/utils/api-client'
export default {
name: 'PhotoFormModal',
props: {
visible: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
photo: {
type: Object,
default: () => {}
},
teams: {
type: Array,
default: () => []
}
},
data() {
return {
form: {
model: {},
rules: {
url: [{ required: true, message: '* 图片地址不能为空', trigger: ['change'] }],
thumbnail: [{ required: true, message: '* 缩略图地址不能为空', trigger: ['change'] }],
name: [{ required: true, message: '* 图片名称不能为空', trigger: ['change'] }]
},
saving: false,
saveErrored: false
},
attachmentSelectModal: {
visible: false
}
}
},
computed: {
modalVisible: {
get() {
return this.visible
},
set(value) {
this.$emit('update:visible', value)
}
},
takeTimeDefaultValue() {
if (this.form.model.takeTime) {
const date = new Date(this.form.model.takeTime)
return datetimeFormat(date, 'YYYY-MM-DD HH:mm:ss')
}
return datetimeFormat(new Date(), 'YYYY-MM-DD HH:mm:ss')
}
},
watch: {
modalVisible(value) {
if (value) {
this.form.model = Object.assign({}, this.photo)
}
},
photo: {
deep: true,
handler(value) {
this.form.model = Object.assign({}, value)
}
}
},
methods: {
onClosed() {},
handleAttachmentSelected({ raw }) {
if (raw.length) {
const { path, thumbPath, name } = raw[0]
this.$set(this.form.model, 'url', encodeURI(path))
this.$set(this.form.model, 'thumbnail', encodeURI(thumbPath))
this.$set(this.form.model, 'name', name)
}
this.attachmentSelectModal.visible = false
},
onTakeTimeChange(value) {
this.form.model.takeTime = value.valueOf()
},
handleSave() {
const _this = this
_this.$refs.photoForm.validate(async valid => {
if (valid) {
_this.form.saving = true
try {
if (_this.form.model.id) {
await apiClient.photo.update(_this.form.model.id, _this.form.model)
} else {
const { data } = await apiClient.photo.create(_this.form.model)
this.form.model = data
}
} catch (e) {
_this.form.saveErrored = true
this.$log.error('Failed to save this photo', e)
} finally {
setTimeout(() => {
_this.form.saving = false
}, 400)
}
}
})
},
handleSaveCallback() {
if (this.form.saveErrored) {
this.form.saveErrored = false
} else {
this.$emit('succeed', this.form.model)
}
}
}
}
</script>