Files
2025-06-13 21:16:12 +08:00

838 lines
24 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="uni-container">
<view class="header">
<view class="back">
<view @click="navigateBack" style="margin-left: 15px;display: flex;align-items: center">
<uni-icons type="back" size="24"></uni-icons>
<text>返回</text>
</view>
</view>
</view>
<uni-forms ref="form" :label-width="90" :model="formData" validateTrigger="submit" err-show-type="toast">
<view class="edit-box">
<view class="title">
<textarea class="uni-input" v-model="formData.title" auto-height
@input="() => autoSaveContent && autoSaveContent()" placeholder="文章标题" />
</view>
<editor-component @textchange="onTextChange" @ready="onEditorReady"></editor-component>
<view class="settings">
<!-- <uni-forms-item name="excerpt" label="文章摘要">
<textarea class="excerpt" placeholder="文章摘要" auto-height v-model="formData.excerpt"
@input="() => autoSaveContent && autoSaveContent()"></textarea>
</uni-forms-item> -->
<uni-forms-item name="thumbnail" label="封面" required>
<view class="thumb-mode">
<radio-group @change="thumbModeChange">
<radio value="1" class="mode" :checked="thumbMode === 1">单图</radio>
<radio value="3" class="mode" :checked="thumbMode === 3">三图</radio>
<radio value="0" class="mode" :checked="thumbMode === 0">无封面图</radio>
</radio-group>
</view>
<view class="thumbnail-list" v-if="thumbMode > 0">
<view class="thumbnail" v-for="(thumbnail, index) in formData.thumbnail"
:key="index">
<template v-if="thumbnail">
<view class="remove-thumbnail" @click="() => removeThumbnail(index)">
<uni-icons type="closeempty" size="16" color="#fff"></uni-icons>
</view>
<cloud-image :src="thumbnail"
width="150px"
height="150px"
mode="scaleToFill">
</cloud-image>
</template>
<template v-else>
<uni-icons @click="setImage(index)" class="placeholder"
type="plusempty" size="60" color="#e8e8e8"></uni-icons>
</template>
</view>
</view>
<view v-if="thumbMode > 0" style="color: #999; font-size: 13px; margin-top: 10px;">
<text>为了保证最佳效果展示;请上传16:9的封面图片</text>
</view>
</uni-forms-item>
<uni-forms-item v-if="false" name="user_id" label="作者" required>
<uni-data-picker v-model="formData.user_id" style="width: 200px;" collection="uni-id-users"
where="role=='uni-cms-author'||role=='admin'"
field="nickname as text, _id as value"></uni-data-picker>
</uni-forms-item>
<uni-forms-item name="category_id" label="分类">
<uni-data-picker v-model="formData.category_id" style="width: 200px;"
collection="uni-cms-categories" field="name as text, _id as value"></uni-data-picker>
</uni-forms-item>
<uni-forms-item name="p_type" label="兵种类型">
<uni-data-picker v-model="formData.p_type" style="width: 200px;" :localdata="pItems"
field="name as text, _id as value"></uni-data-picker>
</uni-forms-item>
<uni-forms-item label="模版">
<image :src="currentTemplateSrc" alt="" class="img_item" mode="widthFix"
@click="openTemplate" />
</uni-forms-item>
<uni-forms-item label="音乐">
<!-- <audio :src="currentMusic" :poster="musicConfig.poster" :name="musicConfig.name"
:author="musicConfig.author" controls v-if="musicConfig.show" @click="openMusic"/> -->
<view class="music-info" v-if="musicInfo.name" @click="toPlay" title="点击播放">
{{musicInfo.playing}}
<!-- <uni-icons type="circle-filled" size="24" v-if="musicInfo.playing" />
<uni-icons type="circle" size="24" v-else /> -->
<image src="https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/music-a.png" mode=""
style="width: 50px;height: 50px;margin-right: 14px;"></image>
{{musicInfo.name}}
<text class="del" @click="musicInfo = {}">×</text>
</view>
<button class="btn" @click="chooseAudio" v-else>
选择音乐
</button>
</uni-forms-item>
<!-- <uni-forms-item name="is_sticky" label="是否置顶">-->
<!-- <switch @change="binddata('is_sticky', $event.detail.value)" style="transform:scale(0.75)" :checked="formData.is_sticky"></switch>-->
<!-- </uni-forms-item>-->
<!-- <uni-forms-item name="is_essence" label="阅读加精">-->
<!-- <switch @change="binddata('is_essence', $event.detail.value)" style="transform:scale(0.75)" :checked="formData.is_essence"></switch>-->
<!-- </uni-forms-item>-->
<!-- <uni-forms-item name="comment_status" label="开放评论">-->
<!-- <uni-data-checkbox v-model="formData.comment_status"-->
<!-- :localdata="formOptions.comment_status_localdata"></uni-data-checkbox>-->
<!-- </uni-forms-item>-->
</view>
<view class="uni-button-group m">
<button class="uni-button" style="width: 100px;margin-right: 10px;" @click="submit(0)">存为草稿</button>
<button type="primary" class="uni-button" style="width: 100px;" @click="submit(1)">发布</button>
</view>
</view>
</uni-forms>
<view class="footer">
<view class="wrap">
<view class="left">
<text class="auto-save" v-if="autoSave">
{{ autoSave.status === 1 ? '自动保存中...' : '已自动保存' }}
</text>
<text class="word-count" v-if="wordCount !== null">共 {{ wordCount }} 字</text>
</view>
<view class="right">
<view class="uni-button-group">
<button class="uni-button" style="width: 100px;margin-right: 10px;"
@click="submit(0)">存为草稿</button>
<button type="primary" class="uni-button" style="width: 100px;" @click="submit(1)">发布</button>
</view>
</view>
</view>
</view>
<uni-drawer class="insert-image-drawer" v-if="drawerWidth" ref="insertImageDrawer" mode="right"
:width="drawerWidth">
<uni-media-library mode="picker" :selected-count="1" :media-tabs="['image']"
@onInsert="onInsertCover"></uni-media-library>
</uni-drawer>
<uni-popup ref="templ" background-color="#fff">
<view class="temps-title">选择模版</view>
<view class="temps">
<radio-group class="grid">
<label v-for="(t, ti) in templateConfig" :key="ti" :id="`temp_${ti + 1}`"
:class="['label', t.checked && 'radio-checked']" @click="templateChange(t.value, t.src, t._id)">
<view :class="['img-area']">
<image :src="t.src" alt="" class="img_item" mode="widthFix" />
</view>
<view class="content">
<radio :value="t.value" :checked="t.checked" class="radio" activeBackgroundColor="red"
@click="templateChange(t.value, t.src, t._id)" />
{{ t.value }}
</view>
</label>
</radio-group>
</view>
<view class="temps-title">
<button type="primary" class="uni-button" style="width: 100%;" @click="submitTemp">确定</button>
</view>
</uni-popup>
<uni-popup ref="musicRef" background-color="#fff">
<view class="music">
<!-- <audio :src="musicConfig.src" :poster="musicConfig.poster" :name="musicConfig.name"
:author="musicConfig.author" controls v-if="musicConfig.show" /> -->
<view class="btn-area">
<button class="btn" @touchend.stop="chooseAudio">
选择音乐
</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
// 引入编辑器组件
import EditorComponent from "@/uni_modules/uni-cms/components/editor/editor.vue";
// 引入表单验证规则
import {
validator
} from '@/uni_modules/uni-cms/common/validator/uni-cms-articles.js';
// 自动保存mixin
import autoSaveMixin from '@/uni_modules/uni-cms/common/auto-save-mixin.js';
import {
parseImageUrl,
parseEditorImage
} from "@/uni_modules/uni-cms/common/parse-image-url";
import cloudImage from "@/components/cloud-image.vue";
// 引入数据库和相关命令
const db = uniCloud.database();
const dbCmd = db.command;
// 定义数据库集合名称
const dbCollectionName = 'uni-cms-articles'
// 获取表单验证规则
function getValidator(fields) {
let result = {}
for (let key in validator) {
if (fields.includes(key)) {
result[key] = validator[key]
}
}
return result
}
export default {
components: {
EditorComponent,
cloudImage
},
mixins: [autoSaveMixin],
data() {
// 初始化formData
let formData = {
"user_id": uniCloud.getCurrentUserInfo().uid,
"category_id": "",
"title": "",
"content": {},
"excerpt": "",
"article_status": 0,
"is_sticky": null,
"is_essence": null,
"comment_status": null,
"thumbnail": [""],
"temp_id":"",
"p_type":"",
}
return {
formData, // 表单数据
formOptions: {
"comment_status_localdata": [{
"value": 0,
"text": "关闭"
},
{
"value": 1,
"text": "开放"
}
]
},
rules: {
...getValidator(Object.keys(formData)) // 表单验证规则
},
formats: {}, // 编辑器格式
wordCount: null, // 字数统计
drawerWidth: 0, // 插入图片抽屉宽度
autoGetCover: true, // 是否自动获取封面
thumbMode: 1, // 封面图展示方式
// 当前模板
currentTemplate: "",
currentTemplateSrc: "/static/temp1.png",
// 模板列表
templateConfig: [
],
// 音乐
musicInfo: {},
currentMusic: "",
innerAudioContext: null,
pItems: [{
text: "空军",
value: "1"
},
{
text: "海军",
value: "2"
},
{
text: "陆军",
value: "3"
},
{
text: "地勤保障",
value: "4"
},
{
text: "武警",
value: "5"
},
]
}
},
async onload() {
// this.formData.category_id = "";
this.formData.p_type = "1";
},
async onReady() {
let res = await db.collection("cms-temp").get();
console.log(res.result.data)
let lst = [];
for (let i = 0 ; i < res.result.data.length ; i++ ) {
lst.push({
value:res.result.data[i].temptele_name,
_id: res.result.data[i]._id,
src: res.result.data[i].temptele_src,
checked: false
})
}
lst[0].checked = true;
this.templateConfig = lst;
this.currentTemplate = this.templateConfig[0]._id;
this.currentTemplateSrc = this.templateConfig[0].src;
this.$refs.form.setRules(this.rules) // 设置表单验证规则
},
mounted() {
const sysinfo = uni.getSystemInfoSync()
this.drawerWidth = sysinfo.windowWidth * .8
},
methods: {
openTemplate() {
this.$refs.templ.open("bottom");
this.$nextTick(() => {
setTimeout(() => {
const dom = document.getElementById(this.currentTemplate)
if (dom) {
dom.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'start'
})
}
}, 10)
})
// this.closeToolSetting()
},
// 选择模板
templateChange(value, src, _id) {
// this.currentTemplate = value;
this.currentTemplateSrc = src;
this.currentTemplate = _id;
for (let i = 0; i < this.templateConfig.length; i++) {
if (this.templateConfig[i].value === value) {
this.templateConfig[i].checked = true;
} else {
this.templateConfig[i].checked = false;
}
}
},
submitTemp() {
for (let i = 0; i < this.templateConfig.length; i++) {
if (this.templateConfig[i].checked === true) {
this.currentTemplateSrc = this.templateConfig[i].src;
this.currentTemplate = this.templateConfig[i]._id;
}
}
this.$refs.templ.close()
},
openMusic() {
this.$refs.musicRef.open("bottom");
},
// 选择音乐
chooseAudio() {
uni.chooseFile({
count: 1,
type: 'file',
extension: ['mp3', 'wav', 'aac'],
success: (res) => {
console.log(res)
console.log(this.musicInfo)
this.musicInfo = res.tempFiles[0]
console.log(this.musicInfo)
console.log(this.musicInfo)
this.uploadMusic(res.tempFiles[0]);
}
});
},
toPlay() {
this.musicInfo.playing = !this.musicInfo.playing;
this.$forceUpdate()
if (!this.innerAudioContext) {
const tempFilePath = this.musicInfo.path;
this.innerAudioContext = uni.createInnerAudioContext();
this.innerAudioContext.autoplay = true;
this.innerAudioContext.src = tempFilePath;
this.innerAudioContext.onPlay(() => {
// console.log('开始播放');
});
this.innerAudioContext.onError((res) => {
console.log(res.errMsg);
console.log(res.errCode);
});
} else {
const tempFilePath = this.musicInfo.path;
this.innerAudioContext.src = tempFilePath;
}
if (this.musicInfo.playing) {
this.innerAudioContext.pause();
} else {
this.innerAudioContext.play();
}
console.log(this.musicInfo.playing)
},
async uploadMusic(filePath) {
try {
console.log(filePath);
const result = await uniCloud.uploadFile({
filePath: filePath.path,
cloudPath: `music/${Date.now()}_${filePath.name}`, // 云存储路径
onUploadProgress: (e) => {
console.log(`进度: ${Math.round(e.loaded / e.total * 100)}%`);
}
});
console.log('上传成功,文件地址:', result.fileID);
console.log(this.musicInfo);
this.musicInfo.music_url_id = result.fileID;
console.log(this.musicInfo);
// 可存储 fileID 或转为永久 URL
const url = await uniCloud.getTempFileURL({
fileList: [result.fileID]
});
console.log('可访问的 URL:', url.fileList[0].tempFileURL);
} catch (err) {
console.error('上传失败:', err);
}
},
// 封面图展示方式改变
thumbModeChange(e) {
const coverCount = Number(e.detail.value)
this.formData.thumbnail = Array.from(new Array(coverCount)).map((item, index) => {
if (this.formData.thumbnail[index]) {
return this.formData.thumbnail[index]
} else {
return ""
}
})
this.thumbMode = coverCount
},
// 监听富文本编辑器内容变化
onTextChange(e) {
// 更新字数统计
this.wordCount = e.detail
// 自动获取编辑器正文内的图片作为封面图
this.autoGetEditorImage()
// 自动保存
this.autoSaveContent && this.autoSaveContent()
},
autoGetEditorImage() {
// 如果不自动获取封面图或者封面图展示方式为不展示,则不执行
if (!this.autoGetCover || this.thumbMode === 0) return
// 获取编辑器中的图片
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.editorCtx.getContents({
success: (res) => {
const {
delta
} = res
const imageList = []
delta.ops.forEach(item => {
if (item.insert && item.insert.image) {
if (!item.attributes || !item.attributes.class || item
.attributes.class.indexOf('uploading') === -1) {
const parseEditorImages = parseEditorImage(item)
imageList.push(parseEditorImages[0])
}
}
})
if (imageList.length <= 0) return
const newThumbnail = [...this.formData.thumbnail]
for (let i = 0; i < newThumbnail.length; i++) {
const thumbnail = newThumbnail[i]
if (thumbnail !== imageList[i].source) {
newThumbnail[i] = imageList[i] || {}
}
}
this.formData.thumbnail = newThumbnail
},
fail: (e) => {
console.error(e)
}
})
}, 1000)
},
// 富文本编辑器初始化完成后触发
onEditorReady(editorCtx) {
if (!editorCtx) return
// 保存editorCtx
this.editorCtx = editorCtx
},
/**
* 验证表单并提交
*/
submit(status) {
if (!this.formData.title) {
// 如果标题为空,提示用户
uni.hideLoading()
return uni.showToast({
icon: 'none',
title: '文章标题必填',
})
}
if (!this.formData.category_id) {
uni.hideLoading()
return uni.showToast({
icon: 'none',
title: '分类必填',
})
}
if (this.formData.thumbnail) {
for (const thumbnail of this.formData.thumbnail) {
if (!thumbnail) {
// 如果封面图为空,提示用户
uni.hideLoading()
return uni.showToast({
icon: 'none',
title: '封面图必填',
})
}
}
}
this.$refs.form.validate().then((res) => {
this.editorCtx.getContents({
success: (e) => {
// 提交表单
console.log(this.musicInfo.music_url_id)
this.submitForm({
...res,
thumbnail: this.formData.thumbnail,
article_status: status,
title: this.formData.title.trim(),
content: e.delta,
publish_date: Date.now(),
temp_id: this.currentTemplate,
music_url_id: this.musicInfo
})
}
})
}).catch((e) => {
console.error(e)
})
},
/**
* 提交表单
*/
async submitForm(value) {
uni.showLoading({
mask: true
})
// 使用 clientDB 提交数据
return db.collection(dbCollectionName).add(value).then((res) => {
uni.showToast({
icon: 'none',
title: value.article_status === 0 ? '保存成功' : '发布成功'
})
try {
// 触发父页面事件,刷新数据
this.getOpenerEventChannel().emit('refreshData')
} catch (e) {}
// 清除自动保存内容
this.clearAutoSaveStorage && this.clearAutoSaveStorage()
setTimeout(() => {
if (value.article_status === 0) {
// 跳转到编辑页面
uni.redirectTo({
url: '/uni_modules/uni-cms/pages/article/edit/edit?id=' + res
.result.id
})
} else {
// 返回上一页
uni.navigateBack()
}
}, 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
// 返回上一页或跳转到文章列表页
navigateBack() {
const pages = getCurrentPages()
if (pages.length > 1) {
// 返回上一页
uni.navigateBack(-1)
} else {
// 跳转到文章列表页
uni.redirectTo({
url: '/uni_modules/uni-cms/pages/article/list/list'
})
}
},
async setImage(index) {
this.coverIndex = index;
const that = this;
uni.chooseImage({
sourceType:["album","camera"],
count: 1,
success: async (res) => {
let t_res = res.tempFiles[0];
if (res.tempFilePaths.length > 0) {
let filePath = res.tempFilePaths[0];
let {
name,
extname
} = that.getFileInfo(filePath)
const uniCloudStorageExtCo = uniCloud.importObject("ext-storage-co");
console.log(`cms/thumbnail/${new Date().getFullYear()}-${(new Date().getMonth()+1).toString().padStart(2,'0')}-${new Date().getDate().toString().padStart(2,'0')}/${name}_${Math.random().toString(36).substr(2, 6)}`)
const uploadFileOptionsRes = await uniCloudStorageExtCo.getUploadFileOptions({
cloudPath: `cms/thumbnail/${name}`
});
console.log(uploadFileOptionsRes)
const uploadRes = await uniCloud.uploadFile({
...uploadFileOptionsRes.uploadFileOptions,
filePath: filePath,
cloudPath: uploadFileOptionsRes.cloudPath
});
await uniCloud.uploadFile({
...uploadFileOptionsRes.uploadFileOptions,
filePath: filePath,
cloudPath: uploadFileOptionsRes.cloudPath,
success: async () => {
const res2 = {
cloudPath: uploadFileOptionsRes.cloudPath,
fileID: uploadFileOptionsRes.fileID,
fileURL: uploadFileOptionsRes.fileURL
};
console.log(t_res)
const selectedMedia = {
src: await this.getSrc(res2.fileID),
alt: name,
type: t_res.type,
size: t_res.size,
duration: 0
}
selectedMedia.cover = selectedMedia.src;
console.log(selectedMedia)
const coverIndex = that.coverIndex !== null ? that.coverIndex : 0
const newThumbnail = [...that.formData.thumbnail]
const parseImages = await selectedMedia.src
newThumbnail[coverIndex] = parseImages
this.formData.thumbnail = newThumbnail
console.log("上传成功", res2);
},
fail: (err) => {
console.log("上传失败", err);
}
});
} else {
console.log("未选中文件")
}
}
});
},
getFileInfo(path) {
const parts = path.split('/').pop();
const [filename, ...extParts] = parts.split('.');
const ext = extParts.length ? extParts.pop() : '';
return {
name: filename,
extname: ext
};
},
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
if (i >= sizes.length) return '> 1 TB';
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
async getSrc(src) {
if (src&&src.substring(0, 8) == "cloud://") {
uniCloud.getTempFileURL({
fileList: [src]
}).then(res=>{
return res.fileList[0].tempFileURL;
})
} else if (src&&src.substring(0, 8) == "qiniu://") {
const uniCloudStorageExtCo = uniCloud.importObject("ext-storage-co");
const res = await uniCloudStorageExtCo.getTempFileURL({src: src});
return res.fileList[0].tempFileURL;
} else {
return src
}
},
openInsertImageDrawer(index) {
this.$refs.insertImageDrawer.open()
this.coverIndex = index
},
async onInsertCover(selectMediaItems) {
console.log(selectMediaItems)
const coverIndex = this.coverIndex !== null ? this.coverIndex : 0
const image = selectMediaItems[0]
const newThumbnail = [...this.formData.thumbnail]
const parseImages = await parseImageUrl(image.src)
newThumbnail[coverIndex] = parseImages[0]
this.$refs.insertImageDrawer.close()
this.formData.thumbnail = newThumbnail
},
removeThumbnail(index) {
const newThumbnail = [...this.formData.thumbnail]
newThumbnail[index] = {}
this.formData.thumbnail = newThumbnail
this.autoGetCover = false
},
}
}
</script>
<style lang="scss">
@import '@/uni_modules/uni-cms/common/style/article-detail.scss';
.edit-box {
padding-bottom: 100px !important;
}
.uni-button-group {
display: flex;
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
background: #fff;
padding: 20px 0;
z-index: 2;
}
.temps-title {
font-size: 32rpx;
font-weight: 600;
text-align: center;
padding: 10px;
}
.temps {
height: 70vh;
overflow-y: auto;
padding: 16px 16px 50px 16px;
.img-area {
position: relative;
border-radius: 5px;
}
.radio-checked {
&::after {
content: "";
position: absolute;
width: calc(100% + 2px);
height: calc(100% + 2px);
left: -1px;
top: -1px;
background-color: red;
z-index: 1;
border-radius: 5px;
}
}
.content {
// margin: 10px auto 20px auto;
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 2;
background: #fff;
width: 100%;
height: 100%;
border-radius: 0 0 5px 5px;
padding: 5px 0;
}
.radio {
zoom: 0.7;
}
}
.img_item {
vertical-align: middle;
width: 100%;
border-radius: 5px 5px 0 0;
z-index: 2;
flex-shrink: 0;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
@media screen and (min-width:800px) {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
}
.label {
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
border-radius: 5px;
z-index: 2;
background-color: #fff;
}
.music-info {
height: 100%;
display: flex;
align-items: center;
cursor: pointer;
border: 2px solid #e5e5e5;
border-radius: 5px;
padding: 5px 15px;
}
.del {
color: red;
margin-left: 20px;
cursor: pointer;
}
</style>