fix(manuscripts): 我的稿件正文改用阅读抽屉渲染富文本
列表把 cms_manuscript.content 当纯文本输出,后台 wangEditor 存的 HTML (<p>、<img>)和早期 AI 草稿的 markdown(##、**)都会原样露出来。 - 列表只留元数据:标题、首行摘要、状态、更新时间、字数;点一行从底部升起阅读抽屉 - 新增 components/manuscript-reader:mp-html 渲染正文(HTML / markdown / 纯文本按内容判定), 支持长按选择、点击图片预览、复制标题、复制正文,有封面时展示封面 - 新增 utils/content-format:格式判定、纯文本化、渲染参数准备 - 搜索改为匹配标题 + 纯文本正文,不再直接匹配带标签的原始 content - 正文每篇只解析一次(加载列表时),摘要、字数、搜索复用同一份纯文本 - 顺带修掉 hero 统计标签在浅色玻璃卡上不可见的问题
This commit is contained in:
@@ -0,0 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="item" class="reader">
|
||||||
|
<view class="reader-mask" @tap="close"></view>
|
||||||
|
<view class="reader-sheet">
|
||||||
|
<view class="reader-sheet__grab"></view>
|
||||||
|
<view class="reader-sheet__head">
|
||||||
|
<view class="reader-sheet__title">{{ item.title || '未命名稿件' }}</view>
|
||||||
|
<view class="reader-sheet__meta">
|
||||||
|
<text class="reader-sheet__status">{{ item.statusText }}</text>
|
||||||
|
<text class="reader-sheet__sep">·</text>
|
||||||
|
<text>{{ item.timeLabel }}</text>
|
||||||
|
<text class="reader-sheet__sep">·</text>
|
||||||
|
<text>{{ item.chars }} 字</text>
|
||||||
|
<text class="reader-sheet__copy" @tap.stop="copyTitle">复制标题</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="reader-sheet__body">
|
||||||
|
<image v-if="item.cover" class="reader-cover" :src="item.cover" mode="widthFix" />
|
||||||
|
<mp-html
|
||||||
|
v-if="payload.html"
|
||||||
|
:content="payload.html"
|
||||||
|
:markdown="payload.markdown"
|
||||||
|
:selectable="true"
|
||||||
|
:preview-img="true"
|
||||||
|
:tag-style="tagStyle"
|
||||||
|
/>
|
||||||
|
<view v-else class="reader-sheet__empty">这篇稿件还没有正文内容。</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="reader-sheet__foot">
|
||||||
|
<view class="reader-btn" @tap="copyContent">复制正文</view>
|
||||||
|
<view class="reader-btn reader-btn--primary" @tap="close">关闭</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 稿件阅读抽屉:列表只放元数据,正文在这里用 mp-html 渲染。
|
||||||
|
*
|
||||||
|
* item 的形状(由列表页整理好,缺内容时传 null 即关闭):
|
||||||
|
* { title, content, cover, statusText, timeLabel, chars }
|
||||||
|
* content 可能是 HTML、markdown 或纯文本,由 getRenderPayload 判断怎么渲染。
|
||||||
|
*/
|
||||||
|
import mpHtml from '@/uni_modules/zero-markdown-view/components/mp-html/mp-html.vue'
|
||||||
|
import { getRenderPayload, toPlainText } from '@/utils/content-format'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ManuscriptReader',
|
||||||
|
components: {
|
||||||
|
mpHtml
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tagStyle: {
|
||||||
|
p: 'margin:0 0 14px 0;',
|
||||||
|
h1: 'font-size:20px;margin:6px 0 12px 0;',
|
||||||
|
h2: 'font-size:18px;margin:6px 0 12px 0;',
|
||||||
|
h3: 'font-size:16px;margin:4px 0 10px 0;',
|
||||||
|
li: 'margin:4px 0;',
|
||||||
|
ul: 'padding-left:22px;margin:10px 0;',
|
||||||
|
ol: 'padding-left:22px;margin:10px 0;',
|
||||||
|
blockquote: 'margin:12px 0;padding:8px 14px;border-left:3px solid #f0b98a;color:#8a7466;background:#fdf6ef;',
|
||||||
|
img: 'max-width:100%;height:auto;border-radius:8px;margin:10px 0;',
|
||||||
|
a: 'color:#0f7dd1;'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
payload() {
|
||||||
|
return getRenderPayload(this.item ? this.item.content : '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
copyTitle() {
|
||||||
|
const title = (this.item && this.item.title) || ''
|
||||||
|
if (!title) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无标题可复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: title,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '标题已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '复制失败,请长按标题手动复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
copyContent() {
|
||||||
|
const text = toPlainText(this.item ? this.item.content : '')
|
||||||
|
if (!text) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无正文可复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: text,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '正文已复制',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '复制失败,可长按正文手动选择',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.reader-mask {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 900;
|
||||||
|
background: rgba(24, 20, 17, 0.42);
|
||||||
|
animation: reader-mask-in 0.18s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 901;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 78vh;
|
||||||
|
padding: 16rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
background: #fffdfb;
|
||||||
|
box-shadow: 0 -12rpx 40rpx rgba(40, 26, 16, 0.18);
|
||||||
|
animation: reader-sheet-in 0.22s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__grab {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
margin: 4rpx auto 18rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: #e3d8d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__head {
|
||||||
|
padding-bottom: 18rpx;
|
||||||
|
border-bottom: 1rpx solid rgba(120, 96, 82, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #1f242b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9b8f88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__status {
|
||||||
|
color: #7d716a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__sep {
|
||||||
|
padding: 0 8rpx;
|
||||||
|
color: #d2c8c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__copy {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 4rpx 0 4rpx 20rpx;
|
||||||
|
color: #c8641f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.85;
|
||||||
|
color: #3f3833;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-cover {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__empty {
|
||||||
|
padding: 40rpx 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #a99e97;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-sheet__foot {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding-top: 16rpx;
|
||||||
|
border-top: 1rpx solid rgba(120, 96, 82, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
line-height: 84rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: #f4f0ec;
|
||||||
|
color: #6d625c;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-btn--primary {
|
||||||
|
background: linear-gradient(135deg, #0f7dd1 0%, #1496f2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reader-mask-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes reader-sheet-in {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+148
-140
@@ -4,10 +4,10 @@
|
|||||||
<common-hero title="我的稿件" :use-image-bg="true"></common-hero>
|
<common-hero title="我的稿件" :use-image-bg="true"></common-hero>
|
||||||
<view class="hero-card">
|
<view class="hero-card">
|
||||||
<view class="hero-title">我的稿件</view>
|
<view class="hero-title">我的稿件</view>
|
||||||
<view class="hero-desc">汇总当前账号创建或参与的稿件记录,可查看状态和内容摘要。</view>
|
<view class="hero-desc">汇总当前账号创建或参与的稿件记录,点开任意一行即可阅读正文。</view>
|
||||||
<view class="hero-stats">
|
<view class="hero-stats">
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<view class="stat-value">{{ manuscripts.length }}</view>
|
<view class="stat-value">{{ rows.length }}</view>
|
||||||
<view class="stat-label">稿件总数</view>
|
<view class="stat-label">稿件总数</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
@@ -44,80 +44,86 @@
|
|||||||
<view class="state-desc">{{ errorText }}</view>
|
<view class="state-desc">{{ errorText }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="!filteredList.length" class="state-card">
|
<view v-else-if="!filteredList.length" class="state-card">
|
||||||
<view class="state-title">暂无稿件</view>
|
<view class="state-title">{{ rows.length ? '没有匹配的稿件' : '暂无稿件' }}</view>
|
||||||
<view class="state-desc">当前账号还没有可展示的稿件,可前往文稿工作台开始生成。</view>
|
<view class="state-desc">
|
||||||
|
{{ rows.length ? '换个关键词再试试,或清空搜索框。' : '当前账号还没有可展示的稿件,可前往文稿工作台开始生成。' }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="card-list">
|
<view v-else class="list-card">
|
||||||
<view v-for="item in filteredList" :key="item.id || item.title" class="data-card">
|
<view class="list-head">
|
||||||
<view class="card-top">
|
<view class="list-head__title">共 {{ filteredList.length }} 篇稿件</view>
|
||||||
<view class="card-main">
|
<view class="list-head__hint">点击任意一行进入阅读</view>
|
||||||
<view class="card-title">{{ item.title || '未命名稿件' }}</view>
|
</view>
|
||||||
<view class="card-subtitle">
|
<view
|
||||||
创建时间:{{ formatDateTime(item.createTime) }} · 更新时间:{{ formatDateTime(item.updateTime) }}
|
v-for="item in filteredList"
|
||||||
</view>
|
:key="item.key"
|
||||||
</view>
|
class="list-row"
|
||||||
<view class="status-tag" :class="`status-tag--${getStatusMeta(item).tone}`">
|
hover-class="list-row--active"
|
||||||
{{ getStatusMeta(item).text }}
|
@tap="openReader(item)"
|
||||||
|
>
|
||||||
|
<view class="list-row__main">
|
||||||
|
<view class="list-row__title">{{ item.title }}</view>
|
||||||
|
<view class="list-row__headline">{{ item.headline || '暂无正文内容' }}</view>
|
||||||
|
<view class="list-row__meta">
|
||||||
|
<text class="list-dot" :class="`list-dot--${item.tone}`"></text>
|
||||||
|
<text class="list-row__status">{{ item.statusText }}</text>
|
||||||
|
<text class="list-row__sep">·</text>
|
||||||
|
<text>{{ item.timeLabel }}</text>
|
||||||
|
<text class="list-row__sep">·</text>
|
||||||
|
<text>{{ item.chars }} 字</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.cover" class="cover-wrap">
|
<view class="list-row__chevron">›</view>
|
||||||
<image class="cover-image" :src="item.cover" mode="aspectFill" />
|
|
||||||
</view>
|
|
||||||
<view class="content-preview">{{ getContentPreview(item.content) }}</view>
|
|
||||||
<view class="card-actions">
|
|
||||||
<view class="card-action" @tap="toggleExpand(item)">{{ expandedId === item.id ? '收起内容' : '查看内容' }}</view>
|
|
||||||
<view class="card-action" @tap="copyTitle(item.title)">复制标题</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="expandedId === item.id" class="content-detail">{{ item.content || '暂无正文内容' }}</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
|
<manuscript-reader :item="reader" @close="closeReader"></manuscript-reader>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CommonHero from '../components/common-hero/common-hero.vue'
|
import CommonHero from '../components/common-hero/common-hero.vue'
|
||||||
|
import ManuscriptReader from '../components/manuscript-reader/manuscript-reader.vue'
|
||||||
import {
|
import {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getManuscriptStatusMeta,
|
getManuscriptStatusMeta,
|
||||||
listManuscript
|
listManuscript
|
||||||
} from '../pages/mine/service'
|
} from '../pages/mine/service'
|
||||||
|
import { toPlainText, truncateText } from '../utils/content-format'
|
||||||
|
|
||||||
|
const HEADLINE_LENGTH = 56
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CommonHero
|
CommonHero,
|
||||||
|
ManuscriptReader
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
errorText: '',
|
errorText: '',
|
||||||
keyword: '',
|
keyword: '',
|
||||||
expandedId: null,
|
rows: [],
|
||||||
manuscripts: []
|
reader: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredList() {
|
filteredList() {
|
||||||
const keyword = String(this.keyword || '').trim().toLowerCase()
|
const keyword = String(this.keyword || '').trim().toLowerCase()
|
||||||
if (!keyword) {
|
if (!keyword) {
|
||||||
return this.manuscripts
|
return this.rows
|
||||||
}
|
}
|
||||||
return this.manuscripts.filter((item) => {
|
return this.rows.filter((item) => {
|
||||||
return (
|
return item.searchText.includes(keyword)
|
||||||
String(item.title || '').toLowerCase().includes(keyword) ||
|
|
||||||
String(item.content || '').toLowerCase().includes(keyword)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
publishedCount() {
|
publishedCount() {
|
||||||
return this.manuscripts.filter((item) => {
|
return this.rows.filter((item) => item.statusText === '通过' || item.statusText === '已发布').length
|
||||||
const text = this.getStatusMeta(item).text
|
|
||||||
return text === '通过' || text === '已发布'
|
|
||||||
}).length
|
|
||||||
},
|
},
|
||||||
pendingCount() {
|
pendingCount() {
|
||||||
return this.manuscripts.length - this.publishedCount
|
return this.rows.length - this.publishedCount
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onShow() {
|
onShow() {
|
||||||
@@ -128,12 +134,36 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
getStatusMeta(item) {
|
/**
|
||||||
return getManuscriptStatusMeta(item)
|
* 稿件 → 列表行视图模型。
|
||||||
|
* 正文可能是 HTML / markdown / 纯文本,这里只做一次纯文本化:
|
||||||
|
* 摘要、搜索、字数都基于它,避免每张卡片反复解析富文本。
|
||||||
|
*/
|
||||||
|
toRow(item, index) {
|
||||||
|
const plainText = toPlainText(item.content)
|
||||||
|
const statusMeta = getManuscriptStatusMeta(item)
|
||||||
|
return {
|
||||||
|
key: item.id || `${index}-${item.title || ''}`,
|
||||||
|
title: item.title || '未命名稿件',
|
||||||
|
headline: truncateText(plainText, HEADLINE_LENGTH),
|
||||||
|
searchText: `${String(item.title || '').toLowerCase()}\n${plainText.toLowerCase()}`,
|
||||||
|
statusText: statusMeta.text,
|
||||||
|
tone: statusMeta.tone,
|
||||||
|
timeLabel: `更新 ${formatDateTime(item.updateTime || item.createTime)}`,
|
||||||
|
chars: plainText.replace(/\s/g, '').length,
|
||||||
|
cover: item.cover || '',
|
||||||
|
content: item.content || ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getContentPreview(content) {
|
applyManuscripts(list, user) {
|
||||||
const text = String(content || '').replace(/\s+/g, ' ').trim()
|
this.rows = (Array.isArray(list) ? list : [])
|
||||||
return text ? `${text.slice(0, 90)}${text.length > 90 ? '...' : ''}` : '暂无内容摘要'
|
.filter((item) => !user.userId || Number(item.userId) === Number(user.userId))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftTime = new Date(String((left.updateTime || left.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
const rightTime = new Date(String((right.updateTime || right.createTime || '')).replace(/-/g, '/')).getTime()
|
||||||
|
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime)
|
||||||
|
})
|
||||||
|
.map((item, index) => this.toRow(item, index))
|
||||||
},
|
},
|
||||||
async loadData(fromPullDown = false) {
|
async loadData(fromPullDown = false) {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
@@ -141,14 +171,7 @@
|
|||||||
try {
|
try {
|
||||||
const user = getCurrentUser()
|
const user = getCurrentUser()
|
||||||
const result = await listManuscript()
|
const result = await listManuscript()
|
||||||
const list = Array.isArray(result) ? result : []
|
this.applyManuscripts(Array.isArray(result) ? result : [], user)
|
||||||
this.manuscripts = list
|
|
||||||
.filter((item) => !user.userId || Number(item.userId) === Number(user.userId))
|
|
||||||
.sort((left, right) => {
|
|
||||||
const leftTime = new Date(String((left.updateTime || left.createTime || '')).replace(/-/g, '/')).getTime()
|
|
||||||
const rightTime = new Date(String((right.updateTime || right.createTime || '')).replace(/-/g, '/')).getTime()
|
|
||||||
return (Number.isNaN(rightTime) ? 0 : rightTime) - (Number.isNaN(leftTime) ? 0 : leftTime)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.errorText = (error && error.message) || '稿件加载失败'
|
this.errorText = (error && error.message) || '稿件加载失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,26 +181,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleExpand(item) {
|
openReader(item) {
|
||||||
this.expandedId = this.expandedId === item.id ? null : item.id
|
this.reader = item
|
||||||
},
|
},
|
||||||
copyTitle(title) {
|
closeReader() {
|
||||||
if (!title) {
|
this.reader = null
|
||||||
uni.showToast({
|
|
||||||
title: '暂无标题可复制',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
uni.setClipboardData({
|
|
||||||
data: title,
|
|
||||||
success: () => {
|
|
||||||
uni.showToast({
|
|
||||||
title: '标题已复制',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
goCreate() {
|
goCreate() {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@@ -204,7 +212,7 @@
|
|||||||
.hero-card,
|
.hero-card,
|
||||||
.toolbar-card,
|
.toolbar-card,
|
||||||
.state-card,
|
.state-card,
|
||||||
.data-card {
|
.list-card {
|
||||||
border-radius: 30rpx;
|
border-radius: 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +245,7 @@
|
|||||||
|
|
||||||
.toolbar-card,
|
.toolbar-card,
|
||||||
.state-card,
|
.state-card,
|
||||||
.data-card {
|
.list-card {
|
||||||
background: rgba(255, 255, 255, 0.94);
|
background: rgba(255, 255, 255, 0.94);
|
||||||
border: 1rpx solid rgba(240, 122, 61, 0.08);
|
border: 1rpx solid rgba(240, 122, 61, 0.08);
|
||||||
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
box-shadow: 0 10rpx 28rpx rgba(76, 49, 35, 0.08);
|
||||||
@@ -253,19 +261,20 @@
|
|||||||
.stat-item {
|
.stat-item {
|
||||||
padding: 18rpx 16rpx;
|
padding: 18rpx 16rpx;
|
||||||
border-radius: 22rpx;
|
border-radius: 22rpx;
|
||||||
background: rgba(255, 255, 255, 0.12);
|
background: rgba(255, 255, 255, 0.62);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: #22272f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
margin-top: 6rpx;
|
margin-top: 6rpx;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: rgba(255, 250, 245, 0.8);
|
color: #8b817c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-card {
|
.toolbar-card {
|
||||||
@@ -336,106 +345,105 @@
|
|||||||
color: #786e69;
|
color: #786e69;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-list {
|
.list-card {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 18rpx;
|
|
||||||
margin-top: 24rpx;
|
margin-top: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-card {
|
.list-head {
|
||||||
padding: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-top {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: baseline;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16rpx;
|
padding: 24rpx 24rpx 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-main {
|
.list-head__title {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #34393f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-head__hint {
|
||||||
|
font-size: 21rpx;
|
||||||
|
color: #a2968e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 26rpx 24rpx;
|
||||||
|
border-top: 1rpx solid rgba(120, 96, 82, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-row--active {
|
||||||
|
background: #f8f3ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-row__main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-title {
|
.list-row__title {
|
||||||
font-size: 30rpx;
|
font-size: 29rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
color: #22272f;
|
color: #22272f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.list-row__headline {
|
||||||
margin-top: 8rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 22rpx;
|
font-size: 23rpx;
|
||||||
line-height: 1.6;
|
color: #8d817a;
|
||||||
color: #8b817c;
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag {
|
.list-row__meta {
|
||||||
padding: 10rpx 16rpx;
|
display: flex;
|
||||||
border-radius: 999rpx;
|
align-items: center;
|
||||||
font-size: 20rpx;
|
margin-top: 12rpx;
|
||||||
font-weight: 700;
|
font-size: 21rpx;
|
||||||
|
color: #a2968e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag--success {
|
.list-dot {
|
||||||
background: #eef9f1;
|
width: 12rpx;
|
||||||
|
height: 12rpx;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-dot--success {
|
||||||
color: #1f8f49;
|
color: #1f8f49;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag--danger {
|
.list-dot--danger {
|
||||||
background: #fff1f0;
|
|
||||||
color: #d4380d;
|
color: #d4380d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag--warning {
|
.list-dot--warning {
|
||||||
background: #fff8e6;
|
|
||||||
color: #ad7b00;
|
color: #ad7b00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-tag--default {
|
.list-dot--default {
|
||||||
background: #f5f5f5;
|
color: #c2b6ae;
|
||||||
color: #8b817c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-wrap {
|
.list-row__status {
|
||||||
margin-top: 18rpx;
|
color: #7d716a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-image {
|
.list-row__sep {
|
||||||
width: 100%;
|
padding: 0 8rpx;
|
||||||
height: 260rpx;
|
color: #d2c8c1;
|
||||||
border-radius: 24rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-preview,
|
.list-row__chevron {
|
||||||
.content-detail {
|
font-size: 34rpx;
|
||||||
margin-top: 18rpx;
|
color: #c9bdb4;
|
||||||
font-size: 24rpx;
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #5d544f;
|
|
||||||
word-break: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-detail {
|
|
||||||
padding: 20rpx;
|
|
||||||
border-radius: 22rpx;
|
|
||||||
background: #fbf7f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 14rpx;
|
|
||||||
margin-top: 18rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-action {
|
|
||||||
padding: 10rpx 18rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: rgba(240, 122, 61, 0.08);
|
|
||||||
color: #c8641f;
|
|
||||||
font-size: 22rpx;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 稿件正文的格式判定与渲染准备。
|
||||||
|
*
|
||||||
|
* `cms_manuscript.content` 里混着两种来源:
|
||||||
|
* 1) 后台 wangEditor 保存的 HTML(`<p>`、`<img src=...>`);
|
||||||
|
* 2) 早期从 AI 结果直接粘进后台的 markdown(`## 标题`、`**加粗**`、`- 列表`)。
|
||||||
|
* 库里没有格式标记,所以只能按内容猜:能识别成 HTML 就按 HTML 渲染,
|
||||||
|
* 否则看是否带 markdown 记号,两者都不是就按纯文本兜底。
|
||||||
|
*
|
||||||
|
* 只做展示需要的粗粒度判定,不追求完整的 HTML / markdown 解析。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const HTML_TAG_RE = /<\/?([a-z][a-z0-9]*)\b[^>]*>/i
|
||||||
|
const MARKDOWN_LINE_RE = /(^|\n)[ \t]{0,3}(#{1,6}[ \t]|[-*+][ \t]|\d+\.[ \t]|>[ \t])/
|
||||||
|
const MARKDOWN_EMPHASIS_RE = /(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(`[^`\n]+`)|(!\[[^\]]*\]\()/
|
||||||
|
|
||||||
|
function decodeEntities(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/</gi, '<')
|
||||||
|
.replace(/>/gi, '>')
|
||||||
|
.replace(/"/gi, '"')
|
||||||
|
.replace(/'/gi, "'")
|
||||||
|
.replace(/&/gi, '&')
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return String(text || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHtmlContent(content) {
|
||||||
|
return HTML_TAG_RE.test(String(content || ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMarkdownContent(content) {
|
||||||
|
const text = String(content || '')
|
||||||
|
if (!text || isHtmlContent(text)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return MARKDOWN_LINE_RE.test(text) || MARKDOWN_EMPHASIS_RE.test(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
function htmlToText(html) {
|
||||||
|
return decodeEntities(
|
||||||
|
String(html)
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
|
.replace(/<\/(p|div|h[1-6]|li|tr|blockquote|section|article|ul|ol|table|pre)>/gi, '\n')
|
||||||
|
.replace(/<li\b[^>]*>/gi, '· ')
|
||||||
|
.replace(/<img\b[^>]*>/gi, '\n[图片]\n')
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
)
|
||||||
|
.replace(/\r\n?/g, '\n')
|
||||||
|
.replace(/[ \t]+\n/g, '\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownToText(markdown) {
|
||||||
|
return String(markdown)
|
||||||
|
.replace(/```[\w-]*\n?([\s\S]*?)```/g, '$1')
|
||||||
|
.replace(/!\[([^\]]*)\]\([^)]*\)/g, (_, alt) => (alt ? `[图片:${alt}]` : '[图片]'))
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
||||||
|
.replace(/^[ \t]{0,3}#{1,6}[ \t]+/gm, '')
|
||||||
|
.replace(/^[ \t]{0,3}>[ \t]?/gm, '')
|
||||||
|
.replace(/^[ \t]{0,3}[-*+][ \t]+/gm, '· ')
|
||||||
|
.replace(/(\*\*\*|___)([^*_]+)\1/g, '$2')
|
||||||
|
.replace(/(\*\*|__)([^*_]+)\1/g, '$2')
|
||||||
|
.replace(/(\*|_)([^*_]+)\1/g, '$2')
|
||||||
|
.replace(/`([^`]+)`/g, '$1')
|
||||||
|
.replace(/^[ \t]{0,3}(-{3,}|\*{3,})[ \t]*$/gm, '')
|
||||||
|
.replace(/\r\n?/g, '\n')
|
||||||
|
.replace(/[ \t]+\n/g, '\n')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 无论来源是 HTML 还是 markdown,都压成可读纯文本(列表摘要、搜索、复制正文都用它)。 */
|
||||||
|
export function toPlainText(content) {
|
||||||
|
const text = String(content || '')
|
||||||
|
if (!text.trim()) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (isHtmlContent(text)) {
|
||||||
|
return htmlToText(text)
|
||||||
|
}
|
||||||
|
if (isMarkdownContent(text)) {
|
||||||
|
return markdownToText(text)
|
||||||
|
}
|
||||||
|
return text.replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 摘要不能直接 slice 原始内容——会把 HTML 标签切一半;这里输入的已是纯文本。 */
|
||||||
|
export function truncateText(text, maxLength = 60) {
|
||||||
|
const plain = String(text || '').replace(/\s+/g, ' ').trim()
|
||||||
|
if (!plain) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return plain.length > maxLength ? `${plain.slice(0, maxLength)}…` : plain
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交给 mp-html 的渲染参数:
|
||||||
|
* - HTML 原样渲染;
|
||||||
|
* - markdown 打开 mp-html 的 markdown 插件;
|
||||||
|
* - 纯文本按空行分段、转义后包成 `<p>`,否则 mp-html 会把换行吃掉。
|
||||||
|
*/
|
||||||
|
export function getRenderPayload(content) {
|
||||||
|
const text = String(content || '')
|
||||||
|
if (!text.trim()) {
|
||||||
|
return {
|
||||||
|
html: '',
|
||||||
|
markdown: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isHtmlContent(text)) {
|
||||||
|
return {
|
||||||
|
html: text,
|
||||||
|
markdown: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isMarkdownContent(text)) {
|
||||||
|
return {
|
||||||
|
html: text,
|
||||||
|
markdown: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
html: escapeHtml(text)
|
||||||
|
.split(/\n{2,}/)
|
||||||
|
.map((paragraph) => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
|
||||||
|
.join(''),
|
||||||
|
markdown: false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user