## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
686 lines
16 KiB
Vue
686 lines
16 KiB
Vue
<template>
|
||
<view class="body">
|
||
<uni-nav-bar
|
||
title="预览"
|
||
leftText="取消"
|
||
left-icon="left"
|
||
right-text="发布"
|
||
backgroundColor="#ffffff"
|
||
color="#000000"
|
||
@clickRight="saveArticle(1)"
|
||
@clickLeft="back()">
|
||
</uni-nav-bar>
|
||
|
||
<!-- 存草稿不发布:草稿仅在「我的作品」中可见 -->
|
||
<view class="draft-bar">
|
||
<button class="draft-btn" :loading="saving" :disabled="saving" @click="saveArticle(0)">
|
||
存为草稿
|
||
</button>
|
||
</view>
|
||
|
||
<view class="v-preview" v-if="temp && temp.temptele_src">
|
||
<image :src="temp.temptele_src" alt="" class="img" mode="widthFix" />
|
||
</view>
|
||
|
||
<view class="" :style="backgrond_color">
|
||
<u-parse :content="titleContent"></u-parse>
|
||
|
||
<view class="">
|
||
<view class="meta">
|
||
<view class="author">
|
||
<cloud-image
|
||
height="64rpx" width="64rpx" borderRadius="50%"
|
||
:src="userInfo.avatar_file.url"></cloud-image>
|
||
<text class="at" :style="color">{{ userInfo.nickname || '' }}</text>
|
||
<text class="split">·</text>
|
||
<text class="date" :style="color">刚刚</text>
|
||
</view>
|
||
</view>
|
||
<view class="">
|
||
<view class="" v-for="(item, index) in cms.cmsLst" :key="index">
|
||
<u-parse
|
||
:content="item.html"
|
||
v-if="item.type === 'image' || item.type === 'video'">
|
||
</u-parse>
|
||
<view class="" v-if="item.type === 'image'"
|
||
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
|
||
<cloud-image
|
||
width="100%"
|
||
height="500rpx"
|
||
mode="aspectFit"
|
||
:src="item.image.src">
|
||
</cloud-image>
|
||
</view>
|
||
<view class="" v-if="item.type === 'video'"
|
||
style="width: 100wv;margin-top: 30rpx;margin-bottom: 30rpx;">
|
||
<video
|
||
style="width: 100%; height: 500rpx;"
|
||
:src="item.video.src"
|
||
enable-danmu
|
||
danmu-btn
|
||
controls>
|
||
</video>
|
||
</view>
|
||
|
||
<view class="" v-if="item.type === 'vote'">
|
||
<uni-card>
|
||
<view class="vote-container">
|
||
<text class="vote-title">{{ item.vote.voteTitle }}</text>
|
||
<text class="deadline">截止时间:{{ item.vote.datetime }}</text>
|
||
<view class="" v-if="item.vote.type === voteType[0]">
|
||
<u-radio-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="groupChange"
|
||
wrap
|
||
>
|
||
|
||
<u-radio
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(radio, radioIndex) in item.vote.voteLst"
|
||
:key="radioIndex"
|
||
:label="radio.value"
|
||
:name="radio.value"
|
||
@change="radioChange"
|
||
style="width: 100%;"
|
||
>
|
||
</u-radio>
|
||
</u-radio-group>
|
||
</view>
|
||
|
||
<view class="" v-if="item.vote.type === voteType[1]">
|
||
<u-checkbox-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="checkboxChange"
|
||
>
|
||
<u-checkbox
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
|
||
:key="checkboxIndex"
|
||
:label="checkbox.value"
|
||
:name="checkbox.value"
|
||
>
|
||
</u-checkbox>
|
||
</u-checkbox-group>
|
||
</view>
|
||
|
||
<view class="" v-if="item.vote.type === voteType[2]">
|
||
<u-checkbox-group
|
||
v-model="item.vote.selectValue"
|
||
placement="column"
|
||
@change="checkboxChange"
|
||
|
||
>
|
||
<view class="">
|
||
<u-checkbox
|
||
:customStyle="{marginBottom: '8px'}"
|
||
v-for="(checkbox, checkboxIndex) in item.vote.voteLst"
|
||
:key="checkboxIndex"
|
||
:label="checkbox.value"
|
||
:name="checkbox.value"
|
||
:disabled="isCheckboxDisabled(item.vote.selectValue, checkbox.value)"
|
||
>
|
||
</u-checkbox>
|
||
</view>
|
||
|
||
</u-checkbox-group>
|
||
</view>
|
||
<view class=""
|
||
v-if="isExpired"
|
||
:disabled="isExpired || !selectedValue"
|
||
style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
|
||
text-align: center;;">
|
||
已截止
|
||
</view>
|
||
|
||
<view class=""
|
||
v-else
|
||
:disabled="isExpired || !selectedValue"
|
||
style="border-top: 3rpx solid #ccc;padding-top: 20rpx;margin-top: 20rpx;
|
||
text-align: center;;">
|
||
{{ isExpired ? '已截止' : '点击登录后投票' }}
|
||
</view>
|
||
</view>
|
||
</uni-card>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<u-tabbar :value="tabbarValue"
|
||
>
|
||
<u-tabbar-item
|
||
v-for="(item, index) in tabbarText"
|
||
:key="index"
|
||
@click="tabbarClick"
|
||
:text="item.text">
|
||
</u-tabbar-item>
|
||
</u-tabbar>
|
||
|
||
<hqs-popup :title="tabbarText[tabbarValue].text" :from="popFrom" :mask-click="maskClick"
|
||
@back="onBack"
|
||
v-model="showPop">
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '模板'">
|
||
<media-list
|
||
:dataType="'template'"
|
||
:pageSize="12"
|
||
:currentTemplate="updateTemplateConfig"
|
||
@update="updateTemplate"
|
||
/>
|
||
</view>
|
||
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '音乐'">
|
||
<media-list
|
||
:dataType="'music'"
|
||
:pageSize="12"
|
||
:currentTemplate="updateTemplateConfig"
|
||
@update="updateTemplate"
|
||
/>
|
||
</view>
|
||
|
||
<view class="page-container" v-if="tabbarText[tabbarValue].text === '排版'">
|
||
</view>
|
||
</hqs-popup>
|
||
|
||
<uni-grid :column="3" :highlight="true" @change="change" :show-border="false">
|
||
<uni-grid-item v-for="(item, index) in gridLst" :index="index" :key="index">
|
||
<view class="grid-item-box">
|
||
<uni-icons :type="item.icon" :size="30"
|
||
:color="item.select_flag?select_icon_color:icon_color" />
|
||
<text class="text" :style="item.select_flag?select_font_color:icon_font_color">
|
||
{{item.value?item.value:0}}
|
||
</text>
|
||
</view>
|
||
</uni-grid-item>
|
||
</uni-grid>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, computed } from 'vue'
|
||
import { onShow } from '@dcloudio/uni-app'
|
||
import { store } from '@/pages3/uni_modules/uni-id-pages/common/store.js'
|
||
import uRadioGroup from "@/uni_modules/uview-ui/components/u-radio-group/u-radio-group.vue"
|
||
import uRadio from "@/uni_modules/uview-ui/components/u-radio/u-radio.vue"
|
||
import uParse from "@/uni_modules/uview-ui/components/u-parse/u-parse.vue"
|
||
import uTabbar from "@/uni_modules/uview-ui/components/u-tabbar/u-tabbar.vue"
|
||
import uTabbarItem from "@/uni_modules/uview-ui/components/u-tabbar-item/u-tabbar-item.vue"
|
||
import mediaList from "@/components/image-grid/media-list.vue"
|
||
import cloudImage from "@/components/cloud-image.vue"
|
||
import uCheckboxGroup from "@/uni_modules/uview-ui/components/u-checkbox-group/u-checkbox-group.vue"
|
||
import uCheckbox from "@/uni_modules/uview-ui/components/u-checkbox/u-checkbox.vue"
|
||
|
||
export default {
|
||
components: {
|
||
uTabbar,
|
||
uTabbarItem,
|
||
mediaList,
|
||
uParse,
|
||
cloudImage,
|
||
uRadioGroup,
|
||
uRadio,
|
||
uCheckboxGroup,
|
||
uCheckbox
|
||
},
|
||
setup() {
|
||
const voteType = ref(["单选(默认)", "多选,无限制", "多选, 最多两项"])
|
||
const voteEndTime = ref('2025-04-20 23:59:59')
|
||
const cms = ref({})
|
||
const temp = ref({})
|
||
const titleContent = ref("")
|
||
const tabbarValue = ref(0)
|
||
const showPop = ref(false)
|
||
const maskClick = ref(true)
|
||
const popFrom = ref('bottom')
|
||
const list = ref(new Array(20).fill(0))
|
||
const currentTemplate = ref("")
|
||
const saving = ref(false)
|
||
|
||
const tabbarText = ref([
|
||
{ text: "模板" },
|
||
{ text: "音乐" },
|
||
{ text: "排版" }
|
||
])
|
||
|
||
const userInfo = computed(() => store.userInfo)
|
||
const isExpired = computed(() => new Date() > new Date(voteEndTime.value))
|
||
|
||
const btn_send_commends_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `background-color: ${temp.value.btn_send_commends_color};`
|
||
})
|
||
|
||
const backgrond_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `background-color: ${temp.value.backgrond_color};`
|
||
})
|
||
|
||
const color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `color: ${temp.value.font_color};`
|
||
})
|
||
|
||
const select_icon_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `${temp.value.select_icon_color};`
|
||
})
|
||
|
||
const icon_color = computed(() => {
|
||
if (!temp.value) return ""
|
||
return `${temp.value.icon_color};`
|
||
})
|
||
|
||
onShow(async () => {
|
||
let storedCms = uni.getStorageSync("cms")
|
||
titleContent.value = storedCms.title_html
|
||
cms.value = storedCms
|
||
await getTemp()
|
||
})
|
||
|
||
async function getTemp() {
|
||
if (currentTemplate.value === "not_image") {
|
||
temp.value = {}
|
||
}
|
||
if (!currentTemplate.value) {
|
||
temp.value = {}
|
||
return
|
||
}
|
||
const res = await uniCloud.importObject("uni-cms-articles").getTemp({
|
||
temp_id: currentTemplate.value
|
||
})
|
||
if (res.code === 200) {
|
||
temp.value = res.data
|
||
}
|
||
}
|
||
|
||
async function updateTemplate(e) {
|
||
currentTemplate.value = e
|
||
await getTemp()
|
||
}
|
||
|
||
function onBack() {
|
||
uni.showToast({ title: 'test' })
|
||
}
|
||
|
||
function tabbarClick(e) {
|
||
tabbarValue.value = e
|
||
showPopupFrom()
|
||
}
|
||
|
||
function showPopupFrom() {
|
||
showPop.value = true
|
||
}
|
||
|
||
function isCheckboxDisabled(arr, name) {
|
||
if (arr.length < 2 || arr.includes(name)) {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function checkboxChange(e) {
|
||
console.log(e)
|
||
}
|
||
|
||
function groupChange(n) {
|
||
console.log('groupChange', n)
|
||
}
|
||
|
||
function radioChange(n) {
|
||
console.log('radioChange', n)
|
||
}
|
||
|
||
function formatDate(date, format) {
|
||
const map = {
|
||
'yyyy': date.getFullYear(),
|
||
'MM': String(date.getMonth() + 1).padStart(2, '0'),
|
||
'dd': String(date.getDate()).padStart(2, '0'),
|
||
'HH': String(date.getHours()).padStart(2, '0'),
|
||
'mm': String(date.getMinutes()).padStart(2, '0'),
|
||
'ss': String(date.getSeconds()).padStart(2, '0')
|
||
}
|
||
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, matched => map[matched])
|
||
}
|
||
|
||
function formatTime() {
|
||
return formatDate(new Date(), 'yyyy-MM-dd HH:mm')
|
||
}
|
||
|
||
/**
|
||
* 保存文章
|
||
* @param {Number} articleStatus 1 发布,0 存草稿
|
||
*/
|
||
async function saveArticle(articleStatus) {
|
||
if (saving.value) return
|
||
|
||
const storedCms = uni.getStorageSync("cms")
|
||
if (!storedCms) {
|
||
return uni.showToast({ title: '内容已丢失,请重新编辑', icon: 'none' })
|
||
}
|
||
if (!storedCms.title || !storedCms.title.trim()) {
|
||
return uni.showToast({ title: '请先填写标题', icon: 'none' })
|
||
}
|
||
// 草稿允许只有标题,发布必须有正文
|
||
const hasContent = Array.isArray(storedCms.cmsLst) && storedCms.cmsLst.length > 0
|
||
if (articleStatus === 1 && !hasContent) {
|
||
return uni.showToast({ title: '正文不能为空', icon: 'none' })
|
||
}
|
||
|
||
saving.value = true
|
||
try {
|
||
// 归属与 IP 由服务端从登录态与请求上下文获取,这里不再传 user_id
|
||
const query = {
|
||
title: storedCms.title,
|
||
title_html: storedCms.title_html,
|
||
title_delta: storedCms.title_delta,
|
||
thumbnail: storedCms.thumbnail,
|
||
p_type: storedCms.p_type,
|
||
category_id: storedCms.category_id,
|
||
temp_id: currentTemplate.value,
|
||
edit_type: "mobile",
|
||
cmsLst: storedCms.cmsLst,
|
||
excerpt: storedCms.excerpt || "",
|
||
article_status: articleStatus,
|
||
}
|
||
|
||
let res
|
||
if (storedCms._id) {
|
||
res = await uniCloud.importObject("uni-cms-articles")
|
||
.update_cms_articles({ ...query, id: storedCms._id })
|
||
} else {
|
||
res = await uniCloud.importObject("uni-cms-articles").add_cms_articles(query)
|
||
}
|
||
|
||
if (!res || res.code !== 200) {
|
||
throw new Error((res && res.msg) || '保存失败')
|
||
}
|
||
|
||
uni.setStorageSync("cms", "")
|
||
uni.showToast({
|
||
title: articleStatus === 1 ? '发布成功' : '已存为草稿',
|
||
icon: 'none'
|
||
})
|
||
setTimeout(() => {
|
||
uni.switchTab({ url: "/pages/my/my" })
|
||
}, 800)
|
||
} catch (e) {
|
||
console.error('保存文章失败:', e)
|
||
uni.showToast({ title: e.message || '保存失败,请重试', icon: 'none' })
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
async function back() {
|
||
uni.navigateBack({
|
||
delta: 1,
|
||
fail: (err) => {
|
||
uni.navigateTo({ url: "/pages2/editCms/editCms" })
|
||
}
|
||
})
|
||
}
|
||
|
||
return {
|
||
voteType,
|
||
voteEndTime,
|
||
cms,
|
||
temp,
|
||
titleContent,
|
||
tabbarValue,
|
||
showPop,
|
||
maskClick,
|
||
popFrom,
|
||
list,
|
||
currentTemplate,
|
||
tabbarText,
|
||
userInfo,
|
||
isExpired,
|
||
btn_send_commends_color,
|
||
backgrond_color,
|
||
color,
|
||
select_icon_color,
|
||
icon_color,
|
||
getTemp,
|
||
updateTemplate,
|
||
onBack,
|
||
tabbarClick,
|
||
showPopupFrom,
|
||
isCheckboxDisabled,
|
||
checkboxChange,
|
||
groupChange,
|
||
radioChange,
|
||
formatDate,
|
||
formatTime,
|
||
saving,
|
||
saveArticle,
|
||
back,
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
|
||
.draft-bar {
|
||
padding: 16rpx 20rpx;
|
||
background-color: #fff;
|
||
border-bottom: 1rpx solid #eee;
|
||
}
|
||
|
||
.draft-btn {
|
||
font-size: 28rpx;
|
||
line-height: 72rpx;
|
||
height: 72rpx;
|
||
color: #c11c1f;
|
||
background-color: #fff;
|
||
border: 1rpx solid #c11c1f;
|
||
border-radius: 36rpx;
|
||
|
||
&::after {
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
.grid-container {
|
||
display: flex;
|
||
gap: 10px;
|
||
padding: 15px;
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.grid-item {
|
||
flex: 1;
|
||
min-width: 0;
|
||
aspect-ratio: 1;
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.page-container {
|
||
padding: 20rpx 0;
|
||
}
|
||
|
||
.rectangle-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
aspect-ratio: 16/9;
|
||
}
|
||
|
||
.meta {
|
||
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-top: 20rpx;
|
||
|
||
.title {
|
||
.text {
|
||
font-size: 40rpx;
|
||
line-height: 66rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.excerpt {
|
||
margin-top: 10rpx;
|
||
|
||
.text {
|
||
font-size: 26rpx;
|
||
line-height: 40rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.author {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
flex-direction: row;
|
||
margin-top: 20rpx;
|
||
|
||
.at,
|
||
.split,
|
||
.date {
|
||
font-size: 26rpx;
|
||
color: #ccc;
|
||
}
|
||
|
||
.split {
|
||
margin: 0 10rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.body {
|
||
padding: 20rpx;
|
||
.cms-body {
|
||
display: flex;
|
||
min-height: 60hv;
|
||
width: 100wv;
|
||
}
|
||
}
|
||
|
||
.v-preview {
|
||
.img {
|
||
width: 100%;
|
||
}
|
||
|
||
.btn-area {
|
||
position: relative;
|
||
top: -50rpx;
|
||
}
|
||
|
||
.red-btn-area {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 10rpx 0;
|
||
margin: 0 40rpx 20rpx 40rpx;
|
||
font-size: 20rpx;
|
||
|
||
.red-btn {
|
||
position: relative;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
font-size: 24rpx;
|
||
height: 40rpx;
|
||
line-height: 40rpx;
|
||
margin: 0;
|
||
background-image: linear-gradient(to bottom, #c11c1f, #5c0201);
|
||
|
||
+.red-btn {
|
||
margin-left: 20rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.red-btn-plain-area {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 10rpx 0;
|
||
margin: 0 40rpx;
|
||
font-size: 21rpx;
|
||
border-top: 2px solid;
|
||
border-bottom: 2px solid;
|
||
border-image: linear-gradient(to right,
|
||
#fff,
|
||
#f44336bf,
|
||
#b13636,
|
||
#f44336bf,
|
||
#fff) 1;
|
||
|
||
.red-btn-plain {
|
||
position: relative;
|
||
color: #b13636;
|
||
font-weight: 700;
|
||
padding-left: 15rpx;
|
||
|
||
+.red-btn-plain {
|
||
margin-left: 10rpx;
|
||
|
||
&::before {
|
||
content: "";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 2px;
|
||
height: 20rpx;
|
||
background-color: #b13636;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
.vote-container {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.vote-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.deadline {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 30rpx;
|
||
display: block;
|
||
}
|
||
|
||
.radio-group {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.option-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin: 15rpx 0;
|
||
}
|
||
|
||
.option-label {
|
||
margin-left: 20rpx;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.vote-btn {
|
||
background-color: #2979FF;
|
||
color: white;
|
||
border-radius: 8rpx;
|
||
font-size: 14px;
|
||
margin-top: 30rpx;
|
||
|
||
&[disabled] {
|
||
background-color: #CCCCCC;
|
||
}
|
||
}
|
||
</style>
|