去除unicloud空间绑定,修复已知错误

This commit is contained in:
fm453
2025-07-16 03:39:53 +08:00
parent c62d15b288
commit b5b8cc5cbb
412 changed files with 507 additions and 40065 deletions

View File

@ -1,49 +0,0 @@
## 0.7.52023-12-18
- 修复 在uni-app x项目部分情况下执行uni-captcha组件的setFocus无效的问题
## 0.7.42023-12-18
- 更新 `package.json` -> `dependencies` 增加 `uni-popup`
## 0.7.32023-11-15
- 更新 uni-popup-captcha.uvue依赖的popup组件直接使用uni_modules下的uni-popup组件
## 0.7.22023-11-07
- 新增 前端组件uni-captcha.uvue、uni-popup-captcha
## 0.7.12023-11-07
- 新增 前端组件uni-captcha.uvue、uni-popup-captcha
## 0.7.02023-10-10
- 新增 支持在`uni-config-center`中配置mode可选值为svg和bmp配置成bmp后可以在uniappx的uvue页面正常显示验证码uvue不支持显示svg验证码
## 0.6.42023-01-16
- 修复 部分情况下APP端无法获取验证码的问题
## 0.6.32023-01-11
- 修复 抖音小程序无法显示的Bug
- 修复 刷新时兼容 device_uuid
## 0.6.12022-06-23
- 修复:部分返回值,不符合响应体规范的问题
## 0.6.02022-05-27
- 新增:支持在`uni-config-center`中根据场景值配置
- 修复:弹窗式验证码,输入内容后点击取消,重新打开验证码的值仍然存在的问题
## 0.5.22022-05-19
- 修复在Vue3的兼容问题
## 0.5.12022-05-18
- 修复在某些情况下微信小程序端验证码显示错误的问题
## 0.5.02022-05-17
- 新增支持在`uni-captcha-co`->`config`配置验证码
## 0.4.12022-05-16
- 新增示例项目
## 0.4.02022-05-16
- 集成创建、刷新、显示验证码的云端一体验证码组件
- 云对象`uni-captcha-co`集成获取验证码的api`getImageCaptcha`
## 0.3.12022-05-13
- 新增 返回值符合响应体规范
## 0.3.02022-05-13
- 新增 支持 uni-config-center 配置
## 0.2.22022-04-25
- 修复 0.2.1 版本引起的使用 image 组件验证码不显示的Bug
## 0.2.12022-04-18
- 更新 优化字体
## 0.2.02022-04-14
- 新增 使用 svg 表现形式更好
- 新增 使用字体,可以任意替换默认字体
- 新增 支持设置字体大小
- 新增 支持忽略某些字符
- 注意 更新之后请重新上传公共模块
## 0.1.02021-03-01
- 调整为uni_modules目录规范

View File

@ -1,180 +0,0 @@
<template>
<view class="captcha-box">
<view class="captcha-img-box">
<image class="loding" src="/uni_modules/uni-captcha/static/run.gif" v-if="loging" mode="widthFix" />
<image class="captcha-img" :class="{opacity:loging}" @click="getImageCaptcha(true)" :src="captchaBase64" mode="widthFix" />
</view>
<input @blur="focusCaptchaInput = false" @focus="focusCaptchaInput = true" :focus="focusCaptchaInput" type="digit" class="captcha" :inputBorder="false"
maxlength="4" v-model="val" placeholder="请输入验证码" :cursor-spacing="cursorSpacing" />
</view>
</template>
<script>
export default {
emits: ["modelValue"],
props: {
cursorSpacing: {
type: Number,
default: 100
},
modelValue: {
type: String,
default: ""
},
value: {
type: String,
default: ""
},
scene: {
type: String,
default: ""
},
focus: {
type: Boolean,
default: false
}
},
data() {
return {
focusCaptchaInput: false,
captchaBase64: "" as string,
loging: false,
val: ""
};
},
watch: {
value: {
handler(value : string) {
// console.log('setvue', value);
this.val = value
},
immediate: true
},
modelValue: {
handler(modelValue : string) {
// console.log('setvue', modelValue);
this.val = modelValue
},
immediate: true
},
scene: {
handler(scene : string) {
if (scene.length != 0) {
this.getImageCaptcha(this.focus)
} else {
uni.showToast({
title: 'scene不能为空',
icon: 'none'
});
}
},
immediate: true
},
val(value : string) {
// console.log('setvue', value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
},
methods: {
setFocus(state:boolean){
this.focusCaptchaInput = state
},
getImageCaptcha(focus : boolean) {
this.loging = true
if (focus) {
this.val = ''
this.focusCaptchaInput = true
}
const uniIdCo = uniCloud.importObject("uni-captcha-co", {
customUI: true
})
uniIdCo.getImageCaptcha({
scene: this.scene,
isUniAppX:true
}).then((result : UTSJSONObject) => {
this.captchaBase64 = (result.getString('captchaBase64') as string)
})
.catch<void>((err : any | null) : void => {
const error = err as UniCloudError
console.error(error)
console.error(error.code)
uni.showToast({
title: error.message,
icon: 'none'
});
})
.finally(()=> {
this.loging = false
})
}
}
}
</script>
<style lang="scss" scoped>
.captcha-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.captcha-img-box,
.captcha {
height: 44px;
}
.captcha {
flex: 1;
}
.captcha-img-box {
position: relative;
background-color: #FEFAE7;
}
.captcha {
background-color: #F8F8F8;
font-size: 14px;
flex: 1;
padding: 0 20rpx;
margin-left: 20rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.captcha-img-box,
.captcha-img,
.loding {
width: 100px;
}
.captcha-img {
/* #ifdef WEB */
cursor: pointer;
/* #endif */
height: 44px;
}
.loding {
z-index: 9;
position: absolute;
width: 30px;
margin:7px 35px;
}
.opacity {
opacity: 0.5;
}
</style>

View File

@ -1,167 +0,0 @@
<template>
<view class="captcha-box">
<view class="captcha-img-box">
<uni-icons class="loding" size="20px" color="#BBB" v-if="loging" type="spinner-cycle"></uni-icons>
<image class="captcha-img" :class="{opacity:loging}" @click="getImageCaptcha" :src="captchaBase64"
mode="widthFix"></image>
</view>
<input @blur="focusCaptchaInput = false" :focus="focusCaptchaInput" type="text" class="captcha"
:inputBorder="false" maxlength="4" v-model="val" placeholder="请输入验证码">
</view>
</template>
<script>
export default {
props: {
modelValue:String,
value:String,
scene: {
type: String,
default () {
return ""
}
},
focus: {
type: Boolean,
default () {
return false
}
}
},
computed:{
val:{
get(){
return this.value||this.modelValue
},
set(value){
// console.log(value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
}
},
data() {
return {
focusCaptchaInput: false,
captchaBase64: "",
loging: false
};
},
watch: {
scene: {
handler(scene) {
if (scene) {
this.getImageCaptcha(this.focus)
} else {
uni.showToast({
title: 'scene不能为空',
icon: 'none'
});
}
},
immediate:true
}
},
methods: {
getImageCaptcha(focus = true) {
this.loging = true
if (focus) {
this.val = ''
this.focusCaptchaInput = true
}
const uniIdCo = uniCloud.importObject("uni-captcha-co", {
customUI: true
})
uniIdCo.getImageCaptcha({
scene: this.scene
}).then(result => {
// console.log(result);
this.captchaBase64 = result.captchaBase64
})
.catch(e => {
uni.showToast({
title: e.message,
icon: 'none'
});
}).finally(e => {
this.loging = false
})
}
}
}
</script>
<style lang="scss" scoped>
.captcha-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.captcha-img-box,
.captcha {
height: 44px;
line-height: 44px;
}
.captcha-img-box {
position: relative;
background-color: #FEFAE7;
}
.captcha {
background-color: #F8F8F8;
font-size: 14px;
flex: 1;
padding: 0 20rpx;
margin-left: 20rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
}
.captcha-img-box,
.captcha-img,
.loding {
height: 44px !important;
width: 100px;
}
.captcha-img{
cursor: pointer;
}
.loding {
z-index: 9;
color: #bbb;
position: absolute;
text-align: center;
line-height: 45px;
animation: rotate 1s linear infinite;
}
.opacity {
opacity: 0.5;
}
@keyframes rotate {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
</style>

View File

@ -1,130 +0,0 @@
<template>
<uni-popup ref="popup" @clickMask="cancel">
<view class="popup-captcha">
<view class="content">
<text class="title">{{title}}</text>
<uni-captcha ref="captcha" :focus="focus" :scene="scene" v-model="val" :cursorSpacing="150"></uni-captcha>
</view>
<view class="button-box">
<text @click="cancel" class="btn cancel">取消</text>
<text @click="confirm" class="btn confirm">确认</text>
</view>
</view>
</uni-popup>
</template>
<script>
let confirmCallBack = ():void=>console.log('未传入回调函数')
export default {
emits:["modelValue","confirm","cancel"],
data() {
return {
focus: false,
val:""
}
},
props: {
modelValue: {
type: String,
default: ""
},
value: {
type: String,
default: ""
},
scene: {
type: String,
default: ""
},
title: {
type: String,
default: "默认标题"
}
},
watch: {
val(val:string) {
// console.log(val);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', val);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', val)
// #endif
if(val.length == 4){
this.confirm()
}
}
},
mounted() {},
methods: {
open(callback: () => void) {
// console.log('callback',callback);
confirmCallBack = callback;
this.focus = true
this.val = "";
(this.$refs['popup'] as ComponentPublicInstance).$callMethod("open");
this.$nextTick(()=>{
(this.$refs['captcha'] as ComponentPublicInstance).$callMethod("getImageCaptcha",true);
})
},
close() {
this.focus = false;
(this.$refs['popup'] as ComponentPublicInstance).$callMethod("close");
},
cancel(){
this.close()
this.$emit("cancel")
},
confirm() {
if (this.val.length != 4) {
return uni.showToast({
title: '请填写验证码',
icon: 'none'
});
}
this.close()
this.$emit('confirm')
confirmCallBack()
}
}
}
</script>
<style lang="scss" scoped>
.popup-captcha {
background-color: #fff;
flex-direction: column;
width: 600rpx;
padding:10px 15px;
border-radius: 10px;
}
.popup-captcha .title {
text-align: center;
font-weight: 700;
margin: 5px 0;
}
.popup-captcha .button-box {
flex-direction: row;
justify-content: space-around;
margin-top: 5px;
}
.popup-captcha .button-box .btn {
flex: 1;
height: 35px;
line-height: 35px;
text-align: center;
}
.popup-captcha .button-box .cancel {
border: 1px solid #eee;
color: #666;
}
.confirm {
background-color: #0070ff;
color: #fff;
margin-left: 5px;
}
</style>

View File

@ -1,140 +0,0 @@
<template>
<uni-popup ref="popup" type="center">
<view class="popup-captcha">
<view class="content">
<text class="title">{{title}}</text>
<uni-captcha :focus="focus" :scene="scene" v-model="val"></uni-captcha>
</view>
<view class="button-box">
<view @click="close" class="btn">取消</view>
<view @click="confirm" class="btn confirm">确认</view>
</view>
</view>
</uni-popup>
</template>
<script>
export default {
data() {
return {
focus: false
}
},
props: {
modelValue:String,
value:String,
scene: {
type: String,
default () {
return ""
}
},
title: {
type: String,
default () {
return ""
}
},
},
computed:{
val:{
get(){
return this.value||this.modelValue
},
set(value){
// console.log(value);
// TODO 兼容 vue2
// #ifdef VUE2
this.$emit('input', value);
// #endif
// TODO 兼容 vue3
// #ifdef VUE3
this.$emit('update:modelValue', value)
// #endif
}
}
},
methods: {
open() {
this.focus = true
this.val = ""
this.$refs.popup.open()
},
close() {
this.focus = false
this.$refs.popup.close()
},
confirm() {
if(!this.val){
return uni.showToast({
title: '请填写验证码',
icon: 'none'
});
}
this.close()
this.$emit('confirm')
}
}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
}
/* #endif */
.popup-captcha {
/* #ifndef APP-NVUE */
display: flex;
max-width: 600px;
/* #endif */
width: 600rpx;
padding-bottom: 0;
background-color: #FFF;
border-radius: 10px;
flex-direction: column;
position: relative;
}
.popup-captcha .content {
padding: 1.3em 0.8em;
}
.popup-captcha .title {
text-align: center;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
font-weight: 400;
font-size: 18px;
overflow: hidden;
text-overflow: ellipsis;
color: #111;
margin-bottom: 15px;
}
.button-box {
height: 44px;
border-top: solid 1px #eee;
flex-direction: row;
align-items: center;
justify-content: space-around;
}
.button-box ,.btn{
height: 44px;
line-height: 44px;
}
.button-box .btn{
flex: 1;
margin: 1px;
text-align: center;
}
.button-box .confirm{
color: #007aff;
border-left: solid 1px #eee;
}
</style>

View File

@ -1,83 +0,0 @@
{
"id": "uni-captcha",
"displayName": "uni-captcha",
"version": "0.7.5",
"description": "云端一体图形验证码组件",
"keywords": [
"captcha",
"图形验证码",
"人机验证",
"防刷",
"防脚本"
],
"repository": "https://gitee.com/dcloud/uni-captcha",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": [
"uni-popup"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}

View File

@ -1,3 +0,0 @@
<h2>
文档已移至 <a href="https://uniapp.dcloud.io/uniCloud/uni-captcha.html" target="_blank">uni-captcha文档</a>
</h2>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
{
"name": "uni-captcha",
"version": "0.7.0",
"description": "uni-captcha",
"main": "index.js",
"homepage": "https://ext.dcloud.net.cn/plugin?id=4048",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-captcha"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}

View File

@ -1,17 +0,0 @@
module.exports = {
"image-captcha":{
"width": 150, //图片宽度
"height": 44, //图片高度
"background": "#FFFAE8", //验证码背景色,设置空字符`''`不使用背景颜色
// "size": 4, //验证码长度,最多 6 个字符
// "noise": 4, //验证码干扰线条数
// "color": false, //字体是否使用随机颜色,当设置`background`后恒为`true`
// "fontSize": 40, //字体大小
// "ignoreChars": '', //忽略那些字符
// "mathExpr": false, //是否使用数学表达式
// "mathMin": 1, //表达式所使用的最小数字
// "mathMax": 9, //表达式所使用的最大数字
// "mathOperator": '' //表达式所使用的运算符,支持 `+`、`-`。不传随机使用
// "expiresDate":180 //验证码过期时间(s)
}
}

View File

@ -1,35 +0,0 @@
// 开发文档: https://uniapp.dcloud.net.cn/uniCloud/cloud-obj
//导入验证码公共模块
const uniCaptcha = require('uni-captcha')
//获取数据库对象
const db = uniCloud.database();
//获取数据表opendb-verify-codes对象
const verifyCodes = db.collection('opendb-verify-codes')
module.exports = {
async getImageCaptcha({
scene,isUniAppX
}) {
//获取设备id
let {
deviceId,
platform
} = this.getClientInfo();
//根据设备id、场景值、状态查找记录是否存在
let res = await verifyCodes.where({
scene,
deviceId,
state: 0
}).limit(1).get()
//如果已存在则调用刷新接口,反之调用插件接口
let action = res.data.length ? 'refresh' : 'create'
//执行并返回结果
let option = {
scene, //来源客户端传递,表示:使用场景值,用于防止不同功能的验证码混用
uniPlatform: platform
}
if(isUniAppX){
option.mode = "bmp"
}
return await uniCaptcha[action](option)
}
}

View File

@ -1,10 +0,0 @@
{
"name": "uni-captcha-co",
"dependencies": {
"uni-captcha": "file:../common/uni-captcha",
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"extensions": {
"uni-cloud-jql": {}
}
}

View File

@ -1,45 +0,0 @@
{
"bsonType": "object",
"properties": {
"_id": {
"description": "ID系统自动生成"
},
"code": {
"bsonType": "string",
"description": "验证码"
},
"create_date": {
"bsonType": "timestamp",
"description": "创建时间"
},
"device_uuid": {
"bsonType": "string",
"description": "设备UUID常用于图片验证码"
},
"email": {
"bsonType": "string",
"description": "邮箱"
},
"expired_date": {
"bsonType": "timestamp",
"description": "过期时间"
},
"ip": {
"bsonType": "string",
"description": "请求时客户端IP地址"
},
"mobile": {
"bsonType": "string",
"description": "手机号码"
},
"scene": {
"bsonType": "string",
"description": "使用验证码的场景login, bind, unbind, pay"
},
"state": {
"bsonType": "int",
"description": "验证状态0 未验证、1 已验证、2 已作废"
}
},
"required": []
}

View File

@ -1,2 +0,0 @@
## 1.0.12023-03-02
- 修复 方法名错误

View File

@ -1,83 +0,0 @@
{
"id": "uni-cloud-s2s",
"displayName": "服务空间与服务器安全通讯模块",
"version": "1.0.1",
"description": "用于解决服务空间与服务器通讯时互相信任问题",
"keywords": [
"安全通讯",
"服务器请求云函数",
"云函数请求服务器"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "unicloud-template-function",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,3 +0,0 @@
# uni-cloud-s2s
文档见:[外部服务器如何与uniCloud安全通讯](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html)

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
{
"name": "uni-cloud-s2s",
"version": "1.0.1",
"description": "",
"keywords": [],
"author": "DCloud",
"main": "index.js",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}

View File

@ -1,41 +0,0 @@
## 1.0.162024-06-21
- 修复 小程序发布时无法上传的Bug
## 1.0.152024-06-20
- 修复 小程序访问文章列表报错的Bug
## 1.0.142024-06-12
- 修复 客户端无法显示文章详情问题的Bug
## 1.0.132023-12-09
- 新增 支持uni-app-x需要[uni-cms](https://ext.dcloud.net.cn/plugin?id=11700)插件版本>=1.0.17
## 1.0.122023-10-17
- 修复 使用腾讯云服务空间时无法加载封面图及正文图片的问题
## 1.0.112023-08-07
- 修复 Vue3下因`parse-scen-result.js`文件导出问题导致无法打包编译的bug
## 1.0.102023-07-14
- 新增 文章预览功能,详见[文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#article-preview)需要uni-cms版本>=1.0.14
- 修复 文章详情页换行重复问题
- 修复 文章列表在数据量小时下拉刷新数据会重复的问题
## 1.0.92023-07-10
- 优化 文章详情页正文可以渲染多个换行
## 1.0.82023-06-21
- 增加 文章列表无图、三图封面样式需要uni-cms版本>=1.0.12
- 优化 文章详情页样式
- 修复 在Vue3下出现 require is not defined 问题
## 1.0.72023-06-07
- 新增 文章详情页面支持播放视频发布视频需要uni-cms版本>=1.0.11
- 优化 文章正文渲染逻辑
- 优化 文章详情页面样式
- 修复 在文章详情页下点击返回按钮无响应问题
## 1.0.62023-04-28
- 修复 文章详情只存在列表时无法渲染的问题
## 1.0.52023-04-24
- 增加 license 文件
## 1.0.42023-04-21
- 优化代码结构,增加代码注释,提高可读性
## 1.0.32023-04-17
- 移除无用schema文件
## 1.0.22023-04-12
- 优化看广告解锁文章交互
## 1.0.12023-04-12
- 优化页面逻辑
## 1.0.02023-04-11
- 插件发布,支持图文内容展示、广告解锁全文功能 [详见文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html)

View File

@ -1,68 +0,0 @@
function parseEditorImage (blocks = []) {
const images = []
if (!Array.isArray(blocks)) {
blocks = [blocks]
}
for (const block of blocks) {
const {insert = {}, attributes = {}} = block
const {'data-custom': custom = ""} = attributes
let parseCustom = custom.split('&').reduce((obj, item) => {
const [key, value] = item.split('=')
if (key && value) {
obj[key] = value
}
return obj
}, {})
images.push({
src: insert.image,
source: parseCustom.source ? parseCustom.source: insert.image
})
}
return images
}
/**
* 解析媒体库/编辑器中的图片
* @param images 图片地址
* @param type {string} 解析类型 media: 媒体库, editor: 编辑器
* @returns {Promise<{src: *, source: *}[]|{src, source: *}[]>}
*/
export async function parseImageUrl (images = [], type = "media") {
if (type === "editor") {
images = parseEditorImage(images).map(item => item.source)
} else {
if (!Array.isArray(images)) {
images = [images]
}
}
if (!images) return null
const tcbFiles = images.filter(item => item.startsWith("cloud://"))
if (tcbFiles.length) {
const res = await uniCloud.getTempFileURL({
fileList: tcbFiles
})
return images.map(image => {
const file = res.fileList.find(item => item.fileID === image)
return {
src: file ? file.tempFileURL : image,
source: image
}
})
} else {
return images.map(image => ({
src: image,
source: image
}))
}
}

View File

@ -1,80 +0,0 @@
export type ParseImageUrlResult = {
src: string
source: string
}
function parseEditorImage (_blocks: any): UTSJSONObject[] {
const images: UTSJSONObject[] = []
let blocks: UTSJSONObject[]
if (!Array.isArray(_blocks)) {
blocks = [_blocks as UTSJSONObject] as UTSJSONObject[]
} else {
blocks = _blocks as UTSJSONObject[]
}
blocks.forEach((block: UTSJSONObject) => {
const insert = block.getJSON('insert')
const attributes = block.getJSON('attributes')
const custom = attributes!.getString('data-custom')
let parseCustom = custom && custom.split('&') ? custom.split('&').reduce((obj: UTSJSONObject, item: string): UTSJSONObject => {
const kv = item.split('=')
if (kv.length > 1) {
obj[kv[0]] = kv[1]
}
return obj
}, {} as UTSJSONObject) : {}
images.push({
src: insert!.getString('image'),
source: parseCustom.getString('source') != null ? parseCustom.getString('source') : insert!.getString('image')
})
})
return images
}
/**
* 解析媒体库/编辑器中的图片
* @param images 图片地址
* @param type {string} 解析类型 media: 媒体库, editor: 编辑器
* @returns {Promise<{src: *, source: *}[]|{src, source: *}[]>}
*/
export async function parseImageUrl (images: any, type: string = "media"): Promise<ParseImageUrlResult[] | null> {
let imagePaths: string[] = []
if (type === "editor") {
imagePaths = parseEditorImage(images).map((item: UTSJSONObject): string => item.getString('source')!)
} else {
if (!Array.isArray(images)) {
imagePaths = [images as string] as string[]
} else {
imagePaths = images
}
}
if (imagePaths.length <= 0) return null
const tcbFiles = imagePaths.filter((item: string): boolean => item.startsWith("cloud://"))
if (tcbFiles.length > 0) {
const res: UniCloudGetTempFileURLResult = await uniCloud.getTempFileURL({
fileList: tcbFiles
})
return imagePaths.map((image: string): ParseImageUrlResult => {
const file = res.fileList.find((item: UniCloudGetTempFileURLResultItem): boolean => item.fileID === image)
return {
src: file ? file.tempFileURL : image,
source: image
} as ParseImageUrlResult
})
} else {
return imagePaths.map((image: string): ParseImageUrlResult => ({
src: image,
source: image
} as ParseImageUrlResult))
}
}

View File

@ -1,28 +0,0 @@
function parseScanResult (scanText) {
const match = scanText.match(/^(.*?):\/\/(.*)/)
if (!match || match.length < 1) {
uni.showToast({
icon: 'none',
title: '未能识别到有效信息'
})
}
const [, protocol, path] = match
switch (protocol) {
case "internallink":
uni.navigateTo({
url: `/${path.replace(/^\//, '')}`,
fail: () => {
uni.showToast({
icon: "none",
title: "访问的路径不存在"
})
}
})
break
}
}
export default parseScanResult

View File

@ -1,28 +0,0 @@
function parseScanResult (scanText: string): void {
const match = scanText.match(/^(.*?):\/\/(.*)/)
if (!match || match.length < 1) {
uni.showToast({
icon: 'none',
title: '未能识别到有效信息'
})
}
const [, protocol, path] = match
switch (protocol) {
case "internallink":
uni.navigateTo({
url: `/${path.replace(/^\//, '')}`,
fail: () => {
uni.showToast({
icon: "none",
title: "访问的路径不存在"
})
}
})
break
}
}
export default parseScanResult

View File

@ -1,93 +0,0 @@
export default function translatePublishTime(timestamp) {
let result = ''
// 获取当前时间
const currentData = new Date()
// 获取发布时间
const date = new Date(timestamp)
// 获取发布年份
const year = date.getFullYear()
// 获取发布月份
const mouth = date.getMonth() + 1
// 获取发布日期
const day = date.getDate()
// 获取发布小时
const hours = date.getHours()
// 获取发布分钟
const minute = date.getMinutes()
// 获取发布秒数
const second = date.getSeconds()
// 获取发布时间戳
const timer = date.getTime()
// 获取当前年份
const currentYear = currentData.getFullYear()
// 获取当前月份
const currentMonth = currentData.getMonth() + 1
// 获取当前日期
const currentDay = currentData.getDate()
// 获取当前小时
const currentHours = currentData.getHours()
// 获取当前分钟
let currentMinute = currentData.getMinutes()
// 获取当前秒数
const currentSecond = currentData.getSeconds()
// 获取当前时间戳
const currentTimer = currentData.getTime()
// 如果时间差小于10秒
if ((currentTimer - timer) < 1000 * 10) {
// 显示刚刚
result = `刚刚`;
// 如果时间差小于60秒
} else if ((currentTimer - timer) < 1000 * 60) {
// 如果当前分钟大于发布分钟
if (currentMinute > minute) {
// 显示秒数差
result = `${(((currentMinute - minute) * 60) + currentSecond - second)}秒前`;
} else {
// 显示秒数差
result = `${(currentSecond - second)}秒前`;
}
// 如果时间差小于1小时
} else if ((currentTimer - timer) < 1000 * (60 * 60)) {
// 如果当前小时大于发布小时
if (currentHours > hours) {
// 显示分钟差
result = `${(((currentHours - hours) * 60) + currentMinute - minute)}分钟前`;
} else {
// 修改 昨天发布的文章时间会出现负数
// 如果当前分钟小于发布分钟
if (currentMinute < minute) {
// 当前分钟加60
currentMinute += 60
}
// 显示分钟差
result = `${(currentMinute - minute)}分钟前`;
}
// 如果时间差小于1天
} else if ((currentTimer - timer) < 1000 * (24 * 60 * 60)) {
// 如果当前日期大于发布日期
if (currentDay > day) {
// 显示小时差
result = `${((currentDay - day) * 24 + currentHours - hours)}小时前`;
} else {
// 修改 跨月-昨天发布的文章时间会出现负数
// 如果当前月份不等于发布月份
if (currentMonth !== mouth) {
// 显示小时差
result = `${(24 + currentHours - hours)}小时前`;
} else {
// 显示小时差
result = `${(currentHours - hours)}小时前`;
}
}
// 如果发布年份等于当前年份
} else if (currentYear === year) {
// 显示月份和日期
result = `${mouth}${day}`;
} else {
// 显示年份、月份和日期
result = `${year}${mouth}${day}`;
}
return result // 返回结果
}

View File

@ -1,93 +0,0 @@
export default function translatePublishTime(timestamp: number): string {
let result: string
// 获取当前时间
const currentData = new Date()
// 获取发布时间
const date = new Date(timestamp)
// 获取发布年份
const year = date.getFullYear()
// 获取发布月份
const mouth = date.getMonth() + 1
// 获取发布日期
const day = date.getDate()
// 获取发布小时
const hours = date.getHours()
// 获取发布分钟
const minute = date.getMinutes()
// 获取发布秒数
const second = date.getSeconds()
// 获取发布时间戳
const timer = date.getTime()
// 获取当前年份
const currentYear = currentData.getFullYear()
// 获取当前月份
const currentMonth = currentData.getMonth() + 1
// 获取当前日期
const currentDay = currentData.getDate()
// 获取当前小时
const currentHours = currentData.getHours()
// 获取当前分钟
let currentMinute = currentData.getMinutes()
// 获取当前秒数
const currentSecond = currentData.getSeconds()
// 获取当前时间戳
const currentTimer = currentData.getTime()
// 如果时间差小于10秒
if ((currentTimer - timer) < 1000 * 10) {
// 显示刚刚
result = `刚刚`;
// 如果时间差小于60秒
} else if ((currentTimer - timer) < 1000 * 60) {
// 如果当前分钟大于发布分钟
if (currentMinute > minute) {
// 显示秒数差
result = `${(((currentMinute - minute) * 60) + currentSecond - second)}秒前`;
} else {
// 显示秒数差
result = `${(currentSecond - second)}秒前`;
}
// 如果时间差小于1小时
} else if ((currentTimer - timer) < 1000 * (60 * 60)) {
// 如果当前小时大于发布小时
if (currentHours > hours) {
// 显示分钟差
result = `${(((currentHours - hours) * 60) + currentMinute - minute)}分钟前`;
} else {
// 修改 昨天发布的文章时间会出现负数
// 如果当前分钟小于发布分钟
if (currentMinute < minute) {
// 当前分钟加60
currentMinute += 60
}
// 显示分钟差
result = `${(currentMinute - minute)}分钟前`;
}
// 如果时间差小于1天
} else if ((currentTimer - timer) < 1000 * (24 * 60 * 60)) {
// 如果当前日期大于发布日期
if (currentDay > day) {
// 显示小时差
result = `${((currentDay - day) * 24 + currentHours - hours)}小时前`;
} else {
// 修改 跨月-昨天发布的文章时间会出现负数
// 如果当前月份不等于发布月份
if (currentMonth !== mouth) {
// 显示小时差
result = `${(24 + currentHours - hours)}小时前`;
} else {
// 显示小时差
result = `${(currentHours - hours)}小时前`;
}
}
// 如果发布年份等于当前年份
} else if (currentYear === year) {
// 显示月份和日期
result = `${mouth}月${day}日`;
} else {
// 显示年份、月份和日期
result = `${year}年${mouth}月${day}日`;
}
return result // 返回结果
}

View File

@ -1,55 +0,0 @@
<template>
<view
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
:key="data?._id"
class="list-item not-cover"
direction="column"
>
<view class="main">
<view>
<text class="title">{{ data?.title }}</text>
</view>
<view class="info">
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { type PropType } from 'vue'
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
type ArticleAuthor = {
_id: string
nickname: string
}
type ArticleItem = {
_id: string
title: string
publish_date: number
thumbnail: string[]
user_id: ArticleAuthor[]
}
export default {
name: "not-cover",
props: {
data: {
type: Object as PropType<ArticleItem>
}
},
methods: {
// 格式化时间戳
publishTime(timestamp: number): string {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,46 +0,0 @@
<template>
<uni-list-item
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
:key="data._id"
class="list-item not-cover"
direction="column"
>
<template v-slot:body>
<view class="main">
<view>
<text class="title">{{ data.title }}</text>
</view>
<view class="info">
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
</view>
</view>
</template>
</uni-list-item>
</template>
<script>
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
export default {
name: "not-cover",
props: {
data: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
// 格式化时间戳
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,50 +0,0 @@
<template>
<view
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
:key="data?._id"
class="list-item"
>
<view class="main">
<text class="title">{{ data?.title }}</text>
<view class="info">
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
</view>
</view>
<image class="thumbnail" :src="data!.thumbnail[0]" mode="aspectFill"></image>
</view>
</template>
<script lang="uts">
import { type PropType } from 'vue'
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
type ArticleAuthor = {
_id: string
nickname: string
}
type ArticleItem = {
_id: string
title: string
publish_date: number
thumbnail: string[]
user_id: ArticleAuthor[]
}
export default {
name: "right-small-cover",
props: {
data: {
type: Object as PropType<ArticleItem>
}
},
methods: {
// 格式化时间戳
publishTime(timestamp: number): string {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,46 +0,0 @@
<template>
<uni-list-item
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
:key="data._id"
class="list-item"
>
<template v-slot:body>
<view class="main">
<text class="title">{{ data.title }}</text>
<view class="info">
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
</view>
</view>
</template>
<template v-slot:footer>
<image class="thumbnail" :src="data.thumbnail[0]" mode="aspectFill"></image>
</template>
</uni-list-item>
</template>
<script>
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
export default {
name: "right-small-cover",
props: {
data: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
// 格式化时间戳
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,63 +0,0 @@
.list-item {
&.not-cover {
.main {
.info {
margin-top: 20rpx;
}
}
}
.main {
display: flex;
justify-content: space-between;
flex-direction: column;
flex: 1;
.title {
font-size: 30rpx;
color: #333333;
}
.thumbnails {
margin: 20rpx 0;
display: flex;
align-items: center;
flex-direction: row;
.img {
flex: 1;
/* #ifndef APP-NVUE */
width: auto;
/* #endif */
height: 200rpx;
border-radius: 8rpx;
margin: 0 10rpx;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
.info {
display: flex;
flex-direction: row;
}
.author,
.publish_date {
font-size: 24rpx;
color: #bbbbbb;
}
.publish_date {
margin-left: 14rpx;
}
}
.thumbnail {
width: 240rpx;
height: 160rpx;
margin-left: 20rpx;
border-radius: 8rpx;
}
}

View File

@ -1,58 +0,0 @@
<template>
<view
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data?._id"
:key="data?._id"
class="list-item"
direction="column"
>
<view class="main">
<text class="title">{{ data?.title }}</text>
<view class="thumbnails">
<image
v-for="image in data?.thumbnail"
:src="image"
mode="aspectFill"
class="img"
></image>
</view>
<view class="info">
<text class="author">{{ data!.user_id!.length > 0 ? data!.user_id[0]!.nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data?.publish_date ?? 0) }}</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { type PropType } from 'vue'
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
type ArticleAuthor = {
_id: string
nickname: string
}
type ArticleItem = {
_id: string
title: string
publish_date: number
thumbnail: string[]
user_id: ArticleAuthor[]
}
export default {
name: "three-cover",
props: {
data: {
type: Object as PropType<ArticleItem>
}
},
methods: {
// 格式化时间戳
publishTime(timestamp: number): string {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,52 +0,0 @@
<template>
<uni-list-item
:to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + data._id"
:key="data._id"
class="list-item"
direction="column"
>
<template v-slot:body>
<view class="main">
<text class="title">{{ data.title }}</text>
<view class="thumbnails">
<image
v-for="image in data.thumbnail"
:src="image"
mode="aspectFill"
class="img"
></image>
</view>
<view class="info">
<text class="author">{{ data.user_id[0] ? data.user_id[0].nickname : '' }}</text>
<text class="publish_date">{{ publishTime(data.publish_date) }}</text>
</view>
</view>
</template>
</uni-list-item>
</template>
<script>
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
export default {
name: "three-cover",
props: {
data: {
type: Object,
default: () => {
return {}
}
}
},
methods: {
// 格式化时间戳
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
}
}
</script>
<style scoped lang="scss">
@import "./style.scss";
</style>

View File

@ -1,99 +0,0 @@
<template>
<refresh @refresh="refresh" @pullingdown="onpullingdown" :display="showRefresh ? 'show' : 'hide'">
<view class="refreshBox">
<!-- 可以自己添加图片路径或base64实现图片 <image class="refreshImg" :src="config[state].img" mode="widthFix" resize="cover"></image> -->
<text class="refreshText">{{config[state].text}}</text>
</view>
</refresh>
</template>
<script>
export default {
data() {
return {
showRefresh:false, // 是否显示刷新
state:0 // 刷新状态0继续下拉执行刷新1释放立即刷新2正在加载中3加载成功
}
},
methods:{
// 下拉刷新回调函数
onpullingdown({pullingDistance,viewHeight}) {
if(pullingDistance < viewHeight){
this.state = 0 // 继续下拉执行刷新
}else{
this.state = 1 // 释放立即刷新
}
},
// 执行刷新
refresh(){
// console.log('refresh');
this.showRefresh = true // 显示刷新
this.state = 2 // 正在加载中
this.$emit('refresh') // 触发refresh事件
}
},
watch: {
// 监听loading变化
loading(loading, oldValue) {
if(!loading){
this.showRefresh = false // 隐藏刷新
this.state = 3 // 加载成功
}
}
},
props: {
loading: {
type:Boolean,
default(){
return false
}
},
config: {
type: Array,
default(){
return [
{
text:"继续下拉执行刷新",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"释放立即刷新",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"正在加载中",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"加载成功",
img:""//可以自己添加图片路径或base64实现图片
}
]
}
},
},
}
</script>
<style lang="scss" scoped>
.refreshBox{
width: 750rpx;
height: 50px;
justify-content: center;
align-items: center;
flex-direction: row;
/* #ifndef APP-PLUS */
margin-top: -50px;
/* #endif */
}
.refreshImg{
width: 55rpx;
height: 55rpx;
z-index: 111;
}
.refreshText{
font-size: 26rpx;
color: #999999;
padding-left: 6rpx;
}
</style>

View File

@ -1,99 +0,0 @@
<template>
<refresh @refresh="refresh" @pullingdown="onpullingdown" :display="showRefresh ? 'show' : 'hide'">
<view class="refreshBox">
<!-- 可以自己添加图片路径或base64实现图片 <image class="refreshImg" :src="config[state].img" mode="widthFix" resize="cover"></image> -->
<text class="refreshText">{{config[state].text}}</text>
</view>
</refresh>
</template>
<script>
export default {
data() {
return {
showRefresh:false, // 是否显示刷新
state:0 // 刷新状态0继续下拉执行刷新1释放立即刷新2正在加载中3加载成功
}
},
methods:{
// 下拉刷新回调函数
onpullingdown({pullingDistance,viewHeight}) {
if(pullingDistance < viewHeight){
this.state = 0 // 继续下拉执行刷新
}else{
this.state = 1 // 释放立即刷新
}
},
// 执行刷新
refresh(){
// console.log('refresh');
this.showRefresh = true // 显示刷新
this.state = 2 // 正在加载中
this.$emit('refresh') // 触发refresh事件
}
},
watch: {
// 监听loading变化
loading(loading, oldValue) {
if(!loading){
this.showRefresh = false // 隐藏刷新
this.state = 3 // 加载成功
}
}
},
props: {
loading: {
type:Boolean,
default(){
return false
}
},
config: {
type: Array,
default(){
return [
{
text:"继续下拉执行刷新",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"释放立即刷新",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"正在加载中",
img:""//可以自己添加图片路径或base64实现图片
},
{
text:"加载成功",
img:""//可以自己添加图片路径或base64实现图片
}
]
}
},
},
}
</script>
<style lang="scss" scoped>
.refreshBox{
width: 750rpx;
height: 50px;
justify-content: center;
align-items: center;
flex-direction: row;
/* #ifndef APP-PLUS */
margin-top: -50px;
/* #endif */
}
.refreshImg{
width: 55rpx;
height: 55rpx;
z-index: 111;
}
.refreshText{
font-size: 26rpx;
color: #999999;
padding-left: 6rpx;
}
</style>

View File

@ -1,167 +0,0 @@
<template>
<view
:class="classList"
v-if="imageData.image != ''"
>
<image
:src="imagePath"
:style="styles"
:alt="imageData.attributes.alt"
class="img"
mode="aspectFill"
@load="imageLoad"
@click="imagePreview"
></image>
</view>
</template>
<script lang="uts">
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.uts";
import type {ParseImageUrlResult} from '@/uni_modules/uni-cms-article/common/parse-image-url.uts';
type ImageAttributes = {
customParams: string | null
width: number | null
height: number | null
alt: string | null
}
type ImageData = {
image: string
attributes: ImageAttributes
}
type ImageCalResult = {
width: number
height: number
}
export default {
name: "render-image",
emits: ['imagePreview'],
props: {
deltaOp: {
type: Object as UTSJSONObject,
default (): UTSJSONObject {
return {}
}
},
reset: {
type: Boolean,
default: false
}
},
data () {
return {
width: 0,
height: 0,
imagePath: ''
}
},
computed: {
imageData (): ImageData {
const insert = this.deltaOp!.getJSON('insert')! as UTSJSONObject
const attributes: UTSJSONObject | null = this.deltaOp!.getJSON('attributes')
console.log(insert, attributes)
return {
image: insert.getString('image')!,
attributes: {
customParams: attributes != null ? attributes.getString('data-custom'): null,
width: attributes != null ? attributes!.getNumber('width'): null,
height: attributes != null ? attributes!.getNumber('height'): null,
alt: attributes != null ? attributes!.getString('alt'): null,
}
} as ImageData
},
classList (): string[] {
return [
'image',
this.reset ? 'reset': ''
] as string[]
},
styles (): string {
let style = ""
if (this.width != 0) {
style += `;width:${this.width}px`
}
if (this.height != 0) {
style += `;height:${this.height}px`
}
return style
}
},
mounted () {
this.loadImagePath()
},
methods: {
async loadImagePath (): Promise<void> {
const {image, attributes} = this.imageData
const parseImages = await parseImageUrl({
insert: {image},
attributes: {
'data-custom': attributes.customParams != null ? attributes.customParams : ""
}
}, "editor")
if (parseImages != null) {
this.imagePath = parseImages[0].src
}
},
imagePreview () {
this.$emit('imagePreview', this.imageData.image)
},
// 图片加载完成
imageLoad(e: ImageLoadEvent) {
const recal = this.wxAutoImageCal(e.detail.width, e.detail.height, 15) // 计算图片宽高
// const image = this.imageData
// ::TODO 关注一下在多端得表现情况
// if (!image.data.attributes.width || Number(image.data.attributes.width) > recal.imageWidth) {
// 如果图片宽度不存在或者图片宽度大于计算出来的宽度,则设置图片宽高
this.width = recal.width
this.height = recal.height
// }
},
// 计算图片宽高
wxAutoImageCal(originalWidth: number, originalHeight: number, imagePadding: number): ImageCalResult {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
let windowWidth: number;
// let windowHeight: number;
let autoWidth: number;
let autoHeight: number;
let results: ImageCalResult = {
width: 0,
height: 0
};
// 计算图片宽度
windowWidth = systemInfo.windowWidth - 2 * imagePadding;
// windowHeight = systemInfo.windowHeight;
if (originalWidth > windowWidth) {//在图片width大于手机屏幕width时候
autoWidth = windowWidth;
autoHeight = (autoWidth * originalHeight) / originalWidth;
results.width = autoWidth;
results.height = autoHeight;
} else {//否则展示原来的数据
results.width = originalWidth;
results.height = originalHeight;
}
return results;
}
}
}
</script>
<style scoped lang="scss">
.image {
margin-bottom: 40rpx;
&.reset {
margin-bottom: 0;
}
.img {
display: flex;
border-radius: 12rpx;
margin: 0 auto;
}
}
</style>

View File

@ -1,131 +0,0 @@
<template>
<view
:class="classList"
v-if="data.data"
>
<image
:src="imagePath"
:class="data.data.class"
:style="styles"
:alt="data.data.attributes.alt || ''"
class="img"
mode="aspectFill"
@load="imageLoad"
@click="imagePreview"
></image>
</view>
</template>
<script>
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.js";
export default {
name: "render-image",
props: {
data: {
type: Object,
default () {
return {}
}
},
className: String,
reset: false
},
data () {
return {
width: 0,
height: 0,
imagePath: ''
}
},
computed: {
classList () {
return [
'image',
this.reset ? 'reset': '',
this.className
]
},
styles () {
let style = this.data.data.style
if (this.width) {
style += `;width:${this.width}px`
}
if (this.height) {
style += `;height:${this.height}px`
}
return style
}
},
mounted () {
this.loadImagePath()
},
methods: {
async loadImagePath () {
const parseImages = await parseImageUrl({
insert: {image: this.data.data.value},
attributes: this.data.data.attributes,
}, "editor")
this.imagePath = parseImages[0].src
},
imagePreview () {
uni.$emit('imagePreview', this.data.data.value)
},
// 图片加载完成
imageLoad(e) {
const recal = this.wxAutoImageCal(e.detail.width, e.detail.height, 15) // 计算图片宽高
// const image = this.data
// ::TODO 关注一下在多端得表现情况
// if (!image.data.attributes.width || Number(image.data.attributes.width) > recal.imageWidth) {
// 如果图片宽度不存在或者图片宽度大于计算出来的宽度,则设置图片宽高
this.width = recal.imageWidth
this.height = recal.imageHeight
// }
},
// 计算图片宽高
wxAutoImageCal(originalWidth, originalHeight, imagePadding = 0) {
// 获取系统信息
const systemInfo = uni.getSystemInfoSync()
let windowWidth = 0, windowHeight = 0;
let autoWidth = 0, autoHeight = 0;
let results = {};
// 计算图片宽度
windowWidth = systemInfo.windowWidth - 2 * imagePadding;
windowHeight = systemInfo.windowHeight;
if (originalWidth > windowWidth) {//在图片width大于手机屏幕width时候
autoWidth = windowWidth;
autoHeight = (autoWidth * originalHeight) / originalWidth;
results.imageWidth = autoWidth;
results.imageHeight = autoHeight;
} else {//否则展示原来的数据
results.imageWidth = originalWidth;
results.imageHeight = originalHeight;
}
return results;
}
}
}
</script>
<style scoped lang="scss">
.image {
margin-bottom: 40rpx;
&.reset {
margin-bottom: 0;
}
.img {
// #ifdef APP-PLUS
display: block;
// #endif
// #ifndef APP-PLUS
display: flex;
// #endif
border-radius: 12rpx;
margin: 0 auto;
}
}
</style>

View File

@ -1,129 +0,0 @@
<template>
<view class="content">
<template v-for="op in content">
<render-text
v-if="op.type === 'paragraph'"
:data="op.data"
:className="op.class"
:style="op.style"
></render-text>
<render-image
v-else-if="op.type === 'image' && op.data.length > 0"
:data="op.data[0]"
:className="op.class"
:style="op.style"
></render-image>
<render-list
v-else-if="op.type === 'list'"
:data="op.data"
:style="op.style"
></render-list>
<view
v-else-if="op.type === 'divider'"
class="divider"
></view>
<render-video
v-else-if="op.type === 'mediaVideo'"
:data="op.data"
></render-video>
<render-unlock-content
v-else-if="op.type === 'unlockContent'"
:adp-id="adConfig.adpId"
:watch-ad-unique-type="adConfig.watchAdUniqueType"
></render-unlock-content>
</template>
</view>
</template>
<script>
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url"
import text from './text.vue'
import image from './image.vue'
import video from './video.vue'
import list from './list.vue'
import unlockContent from './unlock-content.vue'
export default {
name: "render-article-detail",
props: {
content: {
type: Array,
default () {
return []
}
},
contentImages: {
type: Array,
default () {
return []
}
},
adConfig: {
type: Object,
default: {}
}
},
data() {
return {
articleImages: []
}
},
components: {
renderUnlockContent: unlockContent,
renderText: text,
renderImage: image,
renderList: list,
renderVideo: video
},
mounted() {
this.initImage()
},
beforeDestroy() {
uni.$off('imagePreview')
},
methods: {
// 初始化图片
async initImage() {
// 获取所有图片
const parseImages = await parseImageUrl(this.contentImages)
if (parseImages != null) {
this.articleImages = parseImages.map(image => image.src)
}
// 监听图片预览
uni.$on('imagePreview', this.imagePreview)
},
// 点击图片预览
imagePreview(src) {
if (src) {
uni.previewImage({
current: src.split('?')[0], // 当前显示图片的http链接
urls: this.articleImages // 需要预览的图片http链接列表
})
}
},
}
}
</script>
<style scoped lang="scss">
.content {
line-height: 1.75;
font-size: 32rpx;
margin-top: 40rpx;
padding: 0 30rpx 80rpx;
word-break: break-word;
color: #333;
}
.divider {
height: 1px;
background: #d8d8d8;
width: 100%;
margin: 40rpx 0;
}
</style>

View File

@ -1,50 +0,0 @@
<template>
<view :class="['list', data.type]">
<view class="list-item" v-for="(item, index) in data.items">
<text class="dot">{{data.type === 'ordered' ? `${index + 1}.` : '&#8226'}}</text>
<render-text :data="item.data" reset class="reset-default"></render-text>
</view>
</view>
</template>
<script>
import text from './text.vue'
export default {
name: "render-list",
props: {
data: {
type: Object,
default () {
return {}
}
}
},
components: {
renderText: text
},
methods: {
}
}
</script>
<style scoped lang="scss">
.list {
margin-bottom: 40rpx;
}
.list-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
.dot {
margin-right: 10rpx;
}
}
.reset-default {
text-indent: 0;
flex: 1;
}
</style>

View File

@ -1,160 +0,0 @@
<template>
<view :class="classList">
<template v-for="item in data">
<text
v-if="item.type === 'text'"
:class="item.data.class"
:style="item.data.style"
class="text"
>
{{item.data.value}}
</text>
<text
v-if="item.type === 'link'"
:class="item.data.class"
:style="item.data.style"
class="link"
@click="goLink(item.data.attributes.link)"
>
{{item.data.value}}
</text>
<image-item v-else-if="item.type === 'image'" :data="item"></image-item>
<!-- #ifdef H5 -->
<br v-else-if="item.type === 'br'" class="br"/>
<!-- #endif -->
<!-- #ifndef H5 -->
<text v-else-if="item.type === 'br'" class="br">\n</text>
<!-- #endif -->
</template>
</view>
</template>
<script>
import ImageItem from './image.vue'
export default {
name: "render-text",
props: {
data: {
type: Array,
default () {
return []
}
},
className: String,
reset: Boolean
},
computed: {
classList () {
return [
'row-text',
this.className,
this.reset ? 'reset': ''
]
}
},
components: {
ImageItem
},
methods: {
show () {
uni.showToast({
title: 'test',
icon: 'none'
})
},
// 点击链接跳转
goLink(link) {
// 如果链接为空,则返回
if (!link) return
// #ifdef H5
// 在新窗口中打开链接
window.open(link, '_blank')
// #endif
// #ifdef MP
// 微信小程序不支持打开外链,复制链接到剪贴板
uni.setClipboardData({
data: link,
success: () => {
uni.showToast({
title: '链接已复制',
icon: 'none'
})
}
})
// #endif
// #ifdef APP
// 在webview中打开链接
uni.navigateTo({
url: `/uni_modules/uni-cms-article/pages/webview/webview?url=${encodeURIComponent(link)}`
})
// #endif
}
}
}
</script>
<style scoped lang="scss">
.row-text, .br {
margin-bottom: 40rpx;
&.reset {
margin-bottom: 0;
}
}
.header-1,
.header-2,
.header-3,
.header-4,
.header-5,
.header-6 {
font-weight: bold;
}
.header-1 {
font-size: 44rpx;
}
.header-2 {
font-size: 40rpx;
}
.header-3 {
font-size: 38rpx;
}
.header-4 {
font-size: 32rpx;
}
.header-5 {
font-size: 28rpx;
}
.header-6 {
font-size: 24rpx;
}
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.strike {
text-decoration: line-through;
}
.underline {
text-decoration: underline;
}
.link {
color: #0064f9;
text-decoration: underline;
}
</style>

View File

@ -1,216 +0,0 @@
<template>
<view class="unlock-content">
<!-- #ifdef H5 -->
<!-- 等广告支持H5后优化-->
<button class="text" @click="callAd">请观看广告后解锁全文</button>
<!-- #endif -->
<!-- #ifndef H5 -->
<ad-rewarded-video ref="rewardedVideo" :adpid="adpId" :preload="false" :disabled="true" :loadnext="true"
:url-callback="urlCallback" @load="onAdLoad" @close="onAdClose" @error="onAdError"
v-slot:default="{ loading, error }">
<text v-if="error" class="text">广告加载失败</text>
</ad-rewarded-video>
<button v-if="!isLoadError" class="text" @click="callAd" :loading="adLoading">请观看广告后解锁全文</button>
<!-- #endif -->
</view>
</template>
<script>
// 实例化数据库
const db = uniCloud.database()
// 定义解锁记录表名
const unlockContentDBName = 'uni-cms-unlock-record'
export default {
name: "ad",
props: {
adpId: String,
watchAdUniqueType: {
type: String,
default: 'device'
},
},
data() {
return {
currentArticleId: '',
currentPageRoute: '',
adLoading: false,
isLoadError: false
}
},
computed: {
// 回调URL
urlCallback() {
return {
extra: JSON.stringify({
article_id: this.currentArticleId,
unique_id: this.uniqueId,
unique_type: this.watchAdUniqueType
})
}
},
// 是否通过设备观看
watchByDevice() {
return this.watchAdUniqueType === 'device'
},
// 是否通过用户观看
watchByUser() {
return this.watchAdUniqueType === 'user'
},
// 获取唯一ID
uniqueId() {
return this.watchByDevice ? uni.getSystemInfoSync().deviceId : uniCloud.getCurrentUserInfo().uid
}
},
// #ifndef H5
mounted() {
// 获取当前页面信息
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
this.currentArticleId = currentPage.options.id
this.currentPageRoute = currentPage.route
// 如果广告位ID未设置则提示广告无法正常加载
if (!this.adpId) {
uni.showModal({
content: '广告位ID未设置广告无法正常加载',
showCancel: false
})
} else {
// 加载广告
this.$refs.rewardedVideo.load()
}
},
// #endif
methods: {
// 调用广告
callAd() {
// #ifdef H5
// 如果在浏览器中则提示需在App或小程序中操作
return uni.showModal({
content: '需观看广告解锁内容, 但浏览器不支持广告播放, 请在App或小程序中操作',
showCancel: false
})
// #endif
if (this.watchByUser) {
// 登录跳转URL 请根据实际情况修改
const redirectUrl = '/uni_modules/uni-id-pages/pages/login/login-withoutpwd' + (this.currentPageRoute ? '?uniIdRedirectUrl=' + this.currentPageRoute + '?id=' + this.currentArticleId : '')
//::TODO 支持设备与用户
// 如果用户未登录,则提示需要登录
if (uniCloud.getCurrentUserInfo().tokenExpired < Date.now()) {
uni.showModal({
content: '请登录后操作',
success: ({ confirm }) => {
confirm && uni.redirectTo({
url: redirectUrl
});
}
})
}
}
// 显示广告
this.adLoading = true
this.$refs.rewardedVideo.show()
},
// 广告加载成功
onAdLoad() {
this.adLoading && this.$refs.rewardedVideo.show()
console.log('广告数据加载成功');
},
// 广告关闭
onAdClose(e) {
console.log('close', e)
const detail = e.detail
// 轮询3次每次1秒如果3秒内没有查询到解锁记录就提示解锁失败
let i = 3
uni.hideLoading()
this.adLoading = false
// detail.isEnded 为true 说明用户观看了完整视频
if (detail && detail.isEnded) {
uni.showLoading({
title: '正在解锁全文',
timeout: 7000
})
let queryResult = setInterval(async () => {
i--;
// 查询解锁记录
const res = await db.collection(unlockContentDBName).where({
unique_id: this.uniqueId,
article_id: this.currentArticleId,
}).get()
// 1. result.data.length 为0 说明没有解锁记录
// 2. i <= 0 说明已经轮询了3次还是没有解锁记录说明解锁失败
// 3. result.data.length && i > 0 说明已经解锁成功
if (i <= 0) {
console.log('解锁失败', i)
clearInterval(queryResult)
uni.hideLoading()
uni.showToast({
title: '解锁失败!',
icon: 'error',
duration: 2000
});
} else if (res.result && res.result.data.length) {
console.log('解锁成功', i)
clearInterval(queryResult)
uni.hideLoading()
uni.showToast({
title: '解锁成功!',
icon: 'success',
duration: 2000
});
uni.$emit('onUnlockContent')
}
}, 1500);
} else {
uni.showModal({
content: "请观看完整视频后解锁全文",
showCancel: false
})
}
},
onAdError(e) {
// uni.hideLoading()
// this.isLoadError = true
console.error('onaderror: ', e)
}
}
}
</script>
<style scoped lang="scss">
.unlock-content {
text-align: center;
padding: 160rpx 0 60rpx;
position: relative;
margin-top: -140rpx;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 160rpx;
background: linear-gradient(to bottom, transparent, #fff);
}
.text {
border: #f0f0f0 solid 1px;
display: inline-block;
background: #f6f6f6;
border-radius: 10rpx;
font-size: 34rpx;
color: #222;
}
}
</style>

View File

@ -1,44 +0,0 @@
<template>
<view class="video">
<video
class="v"
:src="data.attributes.src"
:poster="data.attributes.poster"
></video>
</view>
</template>
<script>
export default {
name: "render-video",
props: {
data: {
type: Object,
default () {
return {}
}
}
},
data () {
return {
width: 0,
height: 0,
}
},
methods: {
}
}
</script>
<style scoped lang="scss">
.video {
margin-bottom: 40rpx;
.v {
display: block;
width: 100%;
max-width: 500px;
margin: 0 auto;
border-radius: 8rpx;
}
}
</style>

View File

@ -1,54 +0,0 @@
<template>
<view>
<text class="uni-cms-article-icon" :style="{color, 'fontSize': size + 'px', lineHeight: size + 'px'}">{{iconCode}}</text>
</view>
</template>
<script lang="uts">
const icons: UTSJSONObject = {
search: "\ue654",
back: "\ue6b9",
scan: "\ue62a",
closeempty: "\ue66c",
trash: "\ue687",
reload: "\ue6b2",
eye: "\ue651",
'eye-slash': '\ue6b3'
}
export default {
name: 'uni-cms-article-icons',
props: {
type: {
type: String,
default: ""
},
color: {
type: String,
default: '#333333'
},
size: {
type: Number,
default: 16
}
},
computed: {
iconCode(): string {
return icons.getString(this.type) as string
}
}
}
</script>
<style>
@font-face {
font-family: "uni-cms-article-icons";
src: url('/uni_modules/uni-cms-article/static/uniicons.ttf');
}
.uni-cms-article-icon {
font-family: "uni-cms-article-icons";
font-style: normal;
}
</style>

View File

@ -1,371 +0,0 @@
<template>
<unicloud-db
ref='udb'
v-slot:default="{ data, pagination, hasMore, loading, error }"
:collection="collectionList"
:page-size="10"
:loadtime="loadTime"
orderby="publish_date desc"
@load="onListLoad"
@error="onListLoadError"
>
<view
v-if="networkType == 'none'"
class="error-box"
@click="checkNetwork"
>
<image class="disconnect-icon" src="/uni_modules/uni-cms-article/static/disconnection.png" mode="widthFix"></image>
<text class="tip-text">当前网络不可用请点击重试</text>
</view>
<list-view
v-else
class="list-view"
:scroll-y="true"
:refresher-enabled="refresherEnabled"
refresher-default-style="none"
:refresher-triggered="refresherTriggered"
@refresherpulling="refresherpulling"
@refresherrefresh="refresherrefresh"
@scrolltolower="scrolltolower"
>
<list-item slot="refresher" class="refresh-box">
<text class="text">{{ refreshText[refreshState] }}</text>
</list-item>
<!-- 列表渲染 -->
<list-item
v-for="item in articleList"
:class="['list-item', `list-item__thumbnail-${item.thumbnail.length}`]"
:key="item._id"
@click="goToDetailPage(item)"
>
<template v-if="item.thumbnail.length == 0">
<view class="list-item__content">
<view class="list-item__content-title">
<text class="text">{{ item.title }}</text>
</view>
<view class="list-item__content-info">
<view class="list-item__author">
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
</view>
<view class="list-item__publish-date">
<text class="text">{{ publishTime(item.publish_date) }}</text>
</view>
</view>
</view>
</template>
<template v-if="item.thumbnail.length == 1">
<view class="list-item__content">
<view class="list-item__content-title">
<text class="text">{{ item.title }}</text>
</view>
<view class="list-item__content-info">
<view class="list-item__author">
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
</view>
<view class="list-item__publish-date">
<text class="text">{{ publishTime(item.publish_date) }}</text>
</view>
</view>
</view>
<view class="list-item__thumbnails">
<image
v-for="image in item.thumbnail"
:src="image"
mode="aspectFill"
class="list-item__img"
></image>
</view>
</template>
<template v-if="item.thumbnail.length == 3">
<view class="list-item__content">
<view class="list-item__content-title">
<text>{{ item.title }}</text>
</view>
<view class="list-item__thumbnails">
<image
v-for="image in item.thumbnail"
:src="image"
mode="aspectFill"
class="list-item__img"
></image>
</view>
<view class="list-item__content-info">
<view class="list-item__author">
<text class="text">{{ item!.user_id!.length > 0 ? item.user_id[0].nickname : '' }}</text>
</view>
<view class="list-item__publish-date">
<text class="text">{{ publishTime(item.publish_date) }}</text>
</view>
</view>
</view>
</template>
</list-item>
<list-item class="load-state">
<text class="text">{{ loading ? '加载中...' : (hasMore ? '上拉加载更多' : '没有更多数据了') }}</text>
</list-item>
</list-view>
</unicloud-db>
</template>
<script lang="uts">
type ArticleAuthor = {
_id: string
nickname: string
}
type ArticleItem = {
_id: string
title: string
publish_date: number
thumbnail: string[]
user_id: ArticleAuthor[]
}
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url.uts";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time.uts";
import type {ParseImageUrlResult} from '@/uni_modules/uni-cms-article/common/parse-image-url.uts'
export default {
name: "uni-cms-article-list",
emits: ['onRefresh', 'onLoadMore'],
props: {
collectionList: {
type: Array as any[],
default: (): any[] => []
},
loadTime: {
type: String,
default: 'auto'
},
refresherEnabled: {
type: Boolean,
default: false
}
},
data() {
return {
articleList: [] as ArticleItem[],
refresherTriggered: false,
refreshState: 0,
refreshText: [
'继续下拉执行刷新',
'释放立即刷新',
'正在加载中',
'加载成功'
],
networkType: ""
}
},
mounted () {
this.checkNetwork()
},
methods: {
checkNetwork() {
uni.getNetworkType({
success: (res) => {
this.networkType = res.networkType;
}
});
},
publishTime(timestamp: number): string {
return translatePublishTime(timestamp)
},
async onListLoad(data: UTSJSONObject[], ended: boolean, pagination: UTSJSONObject): Promise<void> {
const listData: ArticleItem[] = data.map((item: UTSJSONObject): ArticleItem => {
let articleItem: ArticleItem = {
_id: item.getString('_id')!,
title: item.getString('title')!,
publish_date: item.getNumber('publish_date')!,
thumbnail: [],
user_id: item.getArray<ArticleAuthor>('user_id')! as ArticleAuthor[]
}
if (typeof item.getAny('thumbnail') === 'string') {
articleItem.thumbnail = [item.getAny('thumbnail')! as string]
} else {
articleItem.thumbnail = item.getArray<string>('thumbnail')!
}
return articleItem
})
// 处理cloud://文件链接
for (let i = 0; i < listData.length; i++) {
const article = listData[i]
const parseImages = await parseImageUrl(article.thumbnail)
if (parseImages != null) {
article.thumbnail = parseImages.map((image: ParseImageUrlResult): string => image.src)
}
}
this.articleList = pagination.getNumber('current') == 1 ? listData : this.articleList.concat(listData)
},
refresherrefresh() {
this.refresherTriggered = true
this.refreshState = 2;
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
clear: true,
success: (_: any) => {
this.refresherTriggered = false
this.refreshState = 3
}
})
},
refresherpulling(e: RefresherEvent) {
if (e.detail.dy.toDouble() == 0.0) {
this.refreshState = 0
} else if (e.detail.dy > 45) {
this.refreshState = 1
}
},
scrolltolower() {
(this.$refs['udb'] as UniCloudDBElement)!.loadMore()
},
reLoadList() {
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
clear: true
})
},
goToDetailPage(article: ArticleItem) {
uni.navigateTo({
url: `/uni_modules/uni-cms-article/pages/detail/detail?id=${article._id}&title=${article.title}`
})
},
onListLoadError () {
this.checkNetwork()
}
}
}
</script>
<style scoped lang="scss">
.refresh-box {
display: flex;
align-items: center;
justify-content: center;
.text {
padding: 30rpx 0;
font-size: 26rpx;
color: #999999;
}
}
.error-box {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
margin: 0 auto;
align-items: center;
justify-content: center;
.disconnect-icon {
width: 200rpx;
margin: 0 auto;
}
.tip-text {
font-size: 26rpx;
color: #333;
margin-top: 40rpx;
}
}
.load-state {
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
.text {
font-size: 28rpx;
color: #999999;
}
}
.list-view {
height: 100%;
padding: 0 20rpx;
}
.list-item {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 0;
border-bottom: #f5f5f5 solid 1px;
&__thumbnail-1 {
.list-item__content-title {
height: 88rpx;
}
}
&__thumbnail-3 {
.list-item__thumbnails {
display: flex;
flex-direction: row;
margin: 20rpx -10rpx;
margin-left: -10rpx;
margin-bottom: 0;
}
.list-item__img {
margin: 0 10rpx;
flex: 1;
}
}
&__thumbnails {
margin-left: 20rpx;
}
&__img {
width: 240rpx;
height: 160rpx;
border-radius: 8rpx;
}
&__content {
display: flex;
justify-content: space-between;
flex-direction: column;
flex: 1;
height: 100%;
&-title {
overflow: hidden;
.text {
font-size: 30rpx;
color: #333333;
line-height: 44rpx;
}
}
&-info {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 20rpx;
}
}
&__author,
&__publish-date {
.text {
font-size: 24rpx;
color: #bbbbbb;
}
}
&__author {
margin-right: 14rpx;
}
}
</style>

View File

@ -1,195 +0,0 @@
<template>
<view class="search-bar">
<view :style="{ height: `${navBarHeight}px` }"></view>
<view class="search-bar__content" @click="goToSearchPage">
<view class="search-bar__left" v-if="!showPlaceholder">
<view class="back-icon">
<uni-cms-article-icons type="back" :size="26" color="#333" @click="back"></uni-cms-article-icons>
</view>
</view>
<view class="search-bar__center">
<uni-cms-article-icons type="search" :size="18" color="#c0c4cc"></uni-cms-article-icons>
<text class="search-bar__placeholder" v-if="showPlaceholder">请输入搜索内容</text>
<input
v-else
ref="search-input"
class="search-bar__input"
placeholder="请输入搜索内容"
v-model="searchVal"
confirm-type="search"
:focus="focus"
@confirm="confirm"
/>
<view class="clear-icon" v-if="hasSearchValue" @click="clear">
<uni-cms-article-icons type="closeempty" :size="12" color="#fff"></uni-cms-article-icons>
</view>
</view>
<view class="search-bar__right" v-if="!showPlaceholder">
<!-- <uni-cms-article-icons type="scan" :size="20" color="#c0c4cc" @click="scan"></uni-cms-article-icons>-->
<text class="search-bar__search-text" @click="confirm">搜索</text>
</view>
</view>
</view>
</template>
<script lang="uts">
// import parseScanResult from "@/uni_modules/uni-cms-article/common/parse-scan-result.uts";
export default {
name: 'search-bar',
emits: ['update:modelValue', 'clear', 'confirm'],
data() {
return {
navBarHeight: 44,
searchVal: ""
}
},
props: {
showPlaceholder: {
type: Boolean,
default: false
},
focus: {
type: Boolean,
default: false
},
modelValue: {
type: String,
default: ""
}
},
watch: {
searchVal(newValue) {
this.$emit('update:modelValue', newValue)
},
modelValue: {
immediate: true,
handler(newVal) {
this.searchVal = newVal
}
},
},
computed: {
hasSearchValue(): boolean {
return this.searchVal != ""
}
},
methods: {
back() {
// 获取当前页面数量
const pages = getCurrentPages()
// 定义文章列表页的路径
const pageUrl = '/uni_modules/uni-cms-article/pages/list/list'
// 如果当前页面数量大于1返回上一页
if (pages.length > 1) {
uni.navigateBack({})
} else { // 否则跳转到文章列表页
uni.redirectTo({
url: pageUrl,
fail: (e: RedirectToFail) => {
// 如果跳转失败说明当前页面是tabbar页面需要使用switchTab跳转
if (e.errMsg.indexOf('tabbar') !== -1) {
uni.switchTab({
url: pageUrl
})
}
}
})
}
},
clear() {
this.searchVal = '';
(this.$refs['search-input'] as Element).blur()
this.$emit('clear')
},
confirm() {
(this.$refs['search-input'] as Element).blur()
this.$emit('confirm', this.searchVal)
},
scan() {
// 扫码暂不支持
// uni.scanCode({
// onlyFromCamera: true,
// scanType: ["qrCode"],
// success: (e) => parseScanResult(e.result),
// fail: (e) => {
// console.error(e)
// }
// })
},
goToSearchPage() {
if (!this.showPlaceholder) return
uni.navigateTo({
url: '/uni_modules/uni-cms-article/pages/search/search'
})
},
hideKeyboard() {
(this.$refs['search-input'] as Element).blur()
}
}
}
</script>
<style lang="scss">
.search-bar {
background: #fff;
&__content {
display: flex;
flex-direction: row;
align-items: center;
padding: 10rpx 20rpx;
}
&__left {
margin-left: -20rpx;
}
&__center {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
background: #F8F8F8;
padding: 20rpx;
margin: 0 20rpx;
margin-left: 0;
border-radius: 40rpx;
flex: 1;
}
&__placeholder {
color: #c0c4cc;
font-size: 28rpx;
margin-left: 10rpx;
}
&__input {
margin-left: 10rpx;
flex: 1;
}
&__search-text {
color: #c0402b;
font-size: 28rpx;
}
}
.back-icon {
padding: 10rpx;
}
.clear-icon {
width: 30rpx;
height: 30rpx;
background: #c0c4cc;
border-radius: 15rpx;
margin-right: 0;
margin-left: 10rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -1,6 +0,0 @@
{
"noData": "No Data",
"noNetwork": "Network error",
"toSet": "Go to settings",
"error": "error"
}

View File

@ -1,6 +0,0 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
export default {
en,
'zh-Hans': zhHans
}

View File

@ -1,6 +0,0 @@
{
"noData": "暂无数据",
"noNetwork": "网络异常",
"toSet": "前往设置",
"error": "错误"
}

View File

@ -1,3 +0,0 @@
新增uni-load-state组件这是一个封装数据请求状态的组件。根据uniCloud-db组件提供的参数直接响应对应的效果。
包括加载中、当前页面为空、没有更多数据、上拉加载更多;
加载错误判断如果是断网就引导打开系统网络设置页面。恢复联网后自动触发networkResume方法。

View File

@ -1,171 +0,0 @@
<template>
<view @appear="appear">
<view v-if="state.error">
<view class="box" v-if="networkType == 'none'">
<image class="icon-image" src="/uni_modules/uni-cms-article/static/disconnection.png" mode="widthFix"></image>
<text class="tip-text">{{noNetwork}}</text>
<view class="btn btn-default" @click="openSettings">
<text class="btn-text">{{toSet}}</text>
</view>
</view>
<text class="error" v-else>{{error}}{{JSON.stringify(state.error)}}</text>
</view>
<template v-else>
<!-- #ifdef APP-NVUE -->
<text class="state-text">{{state.loading?'加载中...':(state.hasMore?'上拉加载更多':'没有更多数据了')}}</text>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<uni-load-more class="uni-load-more" :status="state.loading?'loading':(state.hasMore?'hasMore':'noMore')"></uni-load-more>
<!-- #endif -->
</template>
</view>
</template>
<script>
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from './i18n/index.js'
const {
t
} = initVueI18n(messages)
export default {
name: "uni-load-state",
computed: {
noData() {
return t('noData')
},
noNetwork() {
return t('noNetwork')
},
toSet() {
return t('toSet')
},
error() {
return t('error')
}
},
data() {
return {
"networkType": ""
};
},
props: {
state: {
type: Object,
default () {
return {
"loading": true,
"hasMore": false,
"pagination": {
"pages": 0
},
"data": [],
"error": {}
}
}
}
},
mounted() {
uni.onNetworkStatusChange(({
networkType
}) => {
if (this.networkType == 'none' && networkType != 'none') { //之前没网现在有了
this.$emit('networkResume')
}
this.networkType = networkType;
});
uni.getNetworkType({
success: ({
networkType
}) => {
this.networkType = networkType;
}
});
},
methods: {
appear() {
if (!this.state.loading && this.state.hasMore) {
this.$emit('loadMore')
}
},
openSettings() {
if (uni.getSystemInfoSync().platform == "ios") {
var UIApplication = plus.ios.import("UIApplication");
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import("NSURL");
var setting2 = NSURL2.URLWithString("App-prefs:root=General");
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass("android.content.Intent");
var Settings = plus.android.importClass("android.provider.Settings");
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent(Settings.ACTION_SETTINGS);
mainActivity.startActivity(intent);
}
}
}
}
</script>
<style scoped>
.box {
flex: 1;
width: 700rpx;
flex-direction: column;
align-items: center;
justify-content: center;
}
.uni-load-more{
align-items: center;
justify-content: center;
}
.state-text {
text-align: center;
font-size: 26rpx;
width: 690rpx;
padding: 10rpx;
color: #999999;
}
.icon-image {
width: 300rpx;
}
.tip-text {
color: #999999;
font-size: 32rpx;
margin-bottom: 30rpx;
}
.btn {
padding: 5px 10px;
width: 128px;
flex-direction: row;
align-items: center;
justify-content: center;
text-align: center;
}
.btn-text {
color: #999999;
font-size: 15px;
}
.btn-default {
border-color: #999999;
border-style: solid;
border-width: 1px;
border-radius: 3px;
}
.error {
width: 690rpx;
color: #DD524D;
}
</style>

View File

@ -1,35 +0,0 @@
# uni-cms源码使用许可协议
2022年10月
本许可协议是数字天堂北京网络技术有限公司以下简称DCloud对其所拥有著作权的“DCloud uni-cms”以下简称软件提供的使用许可协议。
您对“软件”的复制、使用、修改及分发受本许可协议的条款的约束,如您不接受本协议,则不能使用、复制、修改本软件。
**授权许可范围**
a) 授予您永久性的、全球性的、免费的、非独占的、不可撤销的本软件的源码使用许可,您可以使用这些源码制作自己的应用。
b) 您只能在DCloud产品体系内使用本软件及其源码。您不能将源码修改后运行在DCloud产品体系之外的环境比如客户端脱离uni-app或服务端脱离uniCloud。
c) DCloud未向您授权商标使用许可。您在根据本软件源码制作自己的应用时需以自己的名义发布软件而不是以DCloud名义发布。
d) 本协议不构成代理关系。
DCloud的责任限制 “软件”在提供时不带任何明示或默示的担保。在任何情况下DCloud不对任何人因使用“软件”而引发的任何直接或间接损失承担责任不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
**您的责任限制**
a) 您需要在授权许可范围内使用软件。
b) 您在分发自己的应用时不得侵犯DCloud商标和名誉权利。
c) 您不得进行破解、反编译、套壳等侵害DCloud知识产权的行为。您不得利用DCloud系统漏洞谋利或侵害DCloud利益如您发现DCloud系统漏洞应第一时间通知DCloud。您不得进行攻击DCloud的服务器、网络等妨碍DCloud运营的行为。您不得利用DCloud的产品进行与DCloud争夺开发者的行为。
d) 如您违反本许可协议需承担因此给DCloud造成的损失。
本协议签订地点为中华人民共和国北京市海淀区。
根据发展DCloud可能会对本协议进行修改。修改时DCloud会在产品或者网页中显著的位置发布相关信息以便及时通知到用户。如果您选择继续使用本框架即表示您同意接受这些修改。
条款结束

View File

@ -1,90 +0,0 @@
{
"id": "uni-cms-article",
"displayName": "uni-cms-article",
"version": "1.0.16",
"description": "uni-cms的用户端包括文章展示、搜索、看广告解锁等功能",
"keywords": [
"uni-cms-article",
"cms",
"uni-cms",
"内容管理",
"文章"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "uniapp-template-page",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [
"uni-search-bar",
"uni-nav-bar",
"uni-list"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,223 +0,0 @@
<template>
<scroll-view :scroll-y="true" v-if="!loading">
<view class="meta">
<view class="title">
<text class="text">{{ articleDetail.title }}</text>
</view>
<view class="excerpt">
<text class="text">{{ articleDetail.excerpt }}</text>
</view>
<view class="author">
<template v-if="articleDetail.user_id != null">
<text class="at">{{ articleDetail.user_id?.nickname ?? '' }}</text>
<text class="split">·</text>
</template>
<text class="date">{{ publishTime(articleDetail.publish_date as number) }}</text>
</view>
</view>
<view class="content" v-if="articleDetail.content != null">
<template v-for="(block, index) in articleDetail.content" :key="index">
<view v-if="block.type == 'mediaVideo'">
<video
style="width: 300px; height: 200px; margin: 0 auto 20px;"
:src="(block.data as UTSJSONObject).getJSON('attributes')!.getString('src')!"
:poster="(block.data as UTSJSONObject).getJSON('attributes')!.getString('poster')!"
></video>
</view>
<view v-if="block.type == 'divider'" class="divider"></view>
<view v-if="block.type == 'unlockContent'" class="unlock-content">
<button @click="unlockContent">请观看广告后解锁全文</button>
</view>
<rich-text v-if="block.type == 'rich-text'" :selectable="false" :nodes="block.data" @itemclick="richTextItemClick"></rich-text>
<render-image-component v-if="block.type == 'image'" :deltaOp="block.data" @image-preview="onImagePreview"></render-image-component>
</template>
</view>
</scroll-view>
</template>
<script lang="uts">
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
import RenderImageComponent from '@/uni_modules/uni-cms-article/components/render-article-detail/image.uvue'
type Author = {
_id: string
nickname: string
}
type Content = {
type: string
data: any
}
type Article = {
_id: string | null
title: string | null
content: Content[] | null
excerpt: string | null
publish_date: number | null
user_id: Author | null
thumbnail: string[] | null
content_images: string[] | null
}
const db = uniCloud.databaseForJQL()
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
export default {
components: {
RenderImageComponent
},
data() {
return {
loading: true,
id: "", // 文章ID
title: "", // 文章标题
articleDetail: {} as Article, // 文章详情
// 广告相关配置
adpId: "", // TODO: 请填写广告位ID
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 deviceuser 表示用户唯一device 表示设备唯一
}
},
computed: {
where(): string {
//拼接where条件 查询条件 ,更多详见 https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
return `_id =="${this.id}"`
},
collection(): any[] {
return [
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp(),
db.collection(userDBName).field('_id, nickname').getTemp()
]
}
},
onLoad(event: OnLoadOptions) {
if (event.has('id')) {
this.id = event.get('id') as string
this.load()
}
if (event.has('title')) {
uni.setNavigationBarTitle({
title: event.get('title') as string
})
}
},
methods: {
async load (): Promise<void> {
uni.showLoading({
title: "加载中..."
})
const articledb = db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp()
const userdb = db.collection(userDBName).field('_id, nickname').getTemp()
const res = await db.collection(articledb, userdb).get()
this.loadData(res.data)
this.loading = false
uni.hideLoading()
},
// 格式化发布时间
publishTime(timestamp: number): string {
return translatePublishTime(timestamp)
},
loadData(data: UTSJSONObject[]) {
if (data.length <= 0) return
const detail = data[0]
const user_id = detail.getArray<Author>('user_id')!;
this.articleDetail = {
title: detail.getString('title'),
content: detail.getArray<Content>('content'),
excerpt: detail.getString('excerpt'),
publish_date: detail.getNumber('publish_date'),
thumbnail: detail.getArray<string>('thumbnail'),
user_id: user_id.length > 0 ? user_id[0]: null,
content_images: detail.getArray<string>('content_images')
} as Article
this.title = detail.getString('title')!
uni.setNavigationBarTitle({
title: this.title
})
},
unlockContent () {
uni.showModal({
content: 'uni-app-x 暂不支持观看广告解锁全文',
showCancel: false
})
},
richTextItemClick (e: RichTextItemClickEvent) {
if (e.detail.href != null) {
uni.navigateTo({
url: `/uni_modules/uni-cms-article/pages/webview/webview?url=${encodeURIComponent(e.detail.href as string)}`
})
}
},
onImagePreview (url: string) {
const contentImages = this.articleDetail.content_images != null ? this.articleDetail.content_images: [] as string[]
uni.previewImage({
current: url, // 当前显示图片的http链接
urls: contentImages as string[] // 需要预览的图片http链接列表
})
}
}
}
</script>
<style scoped lang="scss">
.meta {
padding: 20rpx 30rpx 0;
position: relative;
z-index: 1;
.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;
}
}
}
.content {
margin-top: 40rpx;
padding: 0 30rpx 80rpx;
}
.divider {
height: 1px;
width: 100%;
background: rgba(0, 0, 0, 0.1);
}
</style>

View File

@ -1,217 +0,0 @@
<template>
<unicloud-db v-slot:default="{data, loading, error, options}" :collection="collection" :options="formData"
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
@load="loadData"
class="article">
<template v-if="!loading && data">
<view class="meta">
<view class="title">
<text class="text">{{ data.title }}</text>
</view>
<view class="excerpt">
<text class="text">{{ data.excerpt }}</text>
</view>
<view class="author">
<template v-if="data.user_id[0]">
<text class="at">{{ data.user_id[0].nickname || '' }}</text>
<text class="split">·</text>
</template>
<text class="date">{{ publishTime(data.publish_date) }}</text>
</view>
</view>
<render-article-detail
:content="data.content"
:content-images="data.content_images"
:ad-config="{ adpId, watchAdUniqueType }"
></render-article-detail>
</template>
<view class="detail-loading" v-else>
<uni-icons type="spinner-cycle" size="35px"/>
</view>
</unicloud-db>
</template>
<script>
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
const db = uniCloud.database()
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
export default {
components: {
uniNavBar,
renderArticleDetail
},
data() {
return {
id: "", // 文章ID
title: "", // 文章标题
formData: {}, // 表单数据
// 广告相关配置
adpId: "", // TODO: 请填写广告位ID
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 deviceuser 表示用户唯一device 表示设备唯一
}
},
computed: {
where() {
//拼接where条件 查询条件 ,更多详见 https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
return `_id =="${this.id}"`
},
collection() {
return [
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content').getTemp(),
db.collection(userDBName).field('_id, nickname').getTemp()
]
}
},
onReady() {
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData所以不能再 onLoad 中执行
if (this.id) { // ID 不为空,则发起查询
this.$refs.detail.loadData()
} else {
uni.showToast({
icon: 'none',
title: 'id 不能为空'
})
}
},
onLoad(event) {
//获取文章id通常 id 来自上一个页面
if (event.id) {
this.id = event.id
}
// 监听解锁内容事件
uni.$on('onUnlockContent', this.onUnlockContent)
},
onUnload() {
// 页面卸载时,移除监听事件
uni.$off('onUnlockContent', this.onUnlockContent)
},
onPageScroll(e) {
// 根据滚动位置判断是否显示导航栏
if (e.scrollTop > 100) {
uni.setNavigationBarTitle({
title: this.title
})
} else {
uni.setNavigationBarTitle({
title: ''
})
}
},
methods: {
// 将时间戳转换为可读的时间格式
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
// 将文章加入阅读历史
setReadHistory() {
// 获取阅读历史缓存,如果不存在则为空数组
const historyCache = uni.getStorageSync('readHistory') || []
// 过滤掉当前文章的阅读历史
const readHistory = historyCache.filter(item => item.article_id !== this.id)
// 将当前文章的阅读历史添加到数组最前面
readHistory.unshift({
article_id: this.id,
last_time: Date.now()
})
// 将更新后的阅读历史缓存到本地
uni.setStorageSync('readHistory', readHistory)
},
// 加载数据
loadData(data) {
// 设置文章标题
this.title = data.title
// 将文章添加进阅读历史
this.setReadHistory()
},
// 监听解锁内容事件,解锁内容后重新加载数据
async onUnlockContent() {
this.$refs.detail.loadData()
}
}
}
</script>
<style scoped lang="scss">
/* #ifdef APP-NVUE */
.article {
background-color: #fff;
}
/* #endif */
@mixin cp {
padding: 0 30rpx;
}
.detail-loading {
margin: 100rpx auto 0;
width: 35px;
height: 35px;
animation: rotate360 2s linear infinite;
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
transform-origin: center center;
}
100% {
transform: rotate(360deg);
transform-origin: center center;
}
}
.meta {
@include cp;
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;
}
}
}
</style>

View File

@ -1,255 +0,0 @@
<template>
<unicloud-db v-slot:default="{loading, error, options}" :collection="collection" :options="formData"
:getone="true" :where="where" :manual="true" ref="detail" foreignKey="uni-cms-articles.user_id"
@load="loadData"
class="article">
<template v-if="!loading && articleData">
<view class="preview-tip">此页面仅用于临时预览文章链接将会在短期内失效</view>
<view class="meta">
<view class="title">
<text class="text">{{ articleData.title }}</text>
</view>
<view class="excerpt">
<text class="text">{{ articleData.excerpt }}</text>
</view>
<view class="author">
<template v-if="articleData.user_id && articleData.user_id[0]">
<text class="at">{{ articleData.user_id[0].nickname || '' }}</text>
<text class="split">·</text>
</template>
<text class="date">{{ publishTime(articleData.publish_date) }}</text>
</view>
</view>
<render-article-detail
:content="articleData.content"
:content-images="articleData.content_images"
:ad-config="{ adpId, watchAdUniqueType }"
></render-article-detail>
</template>
<view class="detail-loading" v-else>
<uni-icons type="spinner-cycle" size="35px"/>
</view>
</unicloud-db>
</template>
<script>
import uniNavBar from '@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue';
import renderArticleDetail from "@/uni_modules/uni-cms-article/components/render-article-detail/index.vue";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
const db = uniCloud.database()
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
export default {
components: {
uniNavBar,
renderArticleDetail
},
data() {
return {
id: "", // 文章ID
title: "", // 文章标题
secret: "", // 文章预览密钥
formData: {}, // 表单数据
articleData: null, // 文章数据
// 广告相关配置
adpId: "", // TODO: 请填写广告位ID
watchAdUniqueType: "device" // TODO: 观看广告的唯一标识类型,可选值为 user 或者 deviceuser 表示用户唯一device 表示设备唯一
}
},
computed: {
where() {
//拼接where条件 查询条件 ,更多详见 https://uniapp.dcloud.net.cn/uniCloud/unicloud-db?id=jsquery
return `_id =="${this.id}" && preview_secret =="${this.secret}"`
},
collection() {
return [
db.collection(articleDBName).where(this.where).field('user_id,thumbnail,excerpt,publish_date,title,content,preview_secret,preview_expired,article_status').getTemp(),
db.collection(userDBName).field('_id, nickname').getTemp()
]
}
},
onReady() {
// 开始加载数据,修改 where 条件后才开始去加载 clinetDB 的数据 ,需要等组件渲染完毕后才开始执行 loadData所以不能再 onLoad 中执行
if (this.id) { // ID 不为空,则发起查询
this.$refs.detail.loadData()
} else {
uni.showToast({
icon: 'none',
title: 'id 不能为空'
})
}
},
onLoad(event) {
//获取文章id通常 id 来自上一个页面
if (event.id) {
this.id = event.id
this.secret = event.secret
}
// 监听解锁内容事件
uni.$on('onUnlockContent', this.onUnlockContent)
},
onUnload() {
// 页面卸载时,移除监听事件
uni.$off('onUnlockContent', this.onUnlockContent)
},
onPageScroll(e) {
// 根据滚动位置判断是否显示导航栏
if (e.scrollTop > 100) {
uni.setNavigationBarTitle({
title: this.title
})
} else {
uni.setNavigationBarTitle({
title: ''
})
}
},
methods: {
// 将时间戳转换为可读的时间格式
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
// 加载数据
loadData(data) {
if (!data) {
return uni.showModal({
content: "文章不存在/预览密钥不存在",
showCancel: false,
success: () => {
// #ifdef H5
window.close()
// #endif
// #ifndef H5
uni.navigateBack()
// #endif
}
})
}
// 文章已发布,跳转到文章详情页
if (data.article_status === 1) {
uni.showToast({
icon: 'none',
title: '文章已发布'
})
uni.redirectTo({
url: `/uni_modules/uni-cms-article/pages/detail/detail?id=${this.id}`
})
return
}
// 预览已过期,提示用户
if (data.preview_expired < Date.now()) {
return uni.showModal({
content: "预览已失效",
showCancel: false,
success: () => {
// #ifdef H5
window.close()
// #endif
// #ifndef H5
uni.navigateBack()
// #endif
}
})
}
// 设置文章标题
this.title = data.title
this.articleData = data
},
// 监听解锁内容事件,解锁内容后重新加载数据
async onUnlockContent() {
this.$refs.detail.loadData()
}
}
}
</script>
<style scoped lang="scss">
/* #ifdef APP-NVUE */
.article {
background-color: #fff;
}
/* #endif */
@mixin cp {
padding: 0 30rpx;
}
.detail-loading {
margin: 100rpx auto 0;
width: 35px;
height: 35px;
animation: rotate360 2s linear infinite;
}
@keyframes rotate360 {
0% {
transform: rotate(0deg);
transform-origin: center center;
}
100% {
transform: rotate(360deg);
transform-origin: center center;
}
}
.meta {
@include cp;
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;
}
}
}
.preview-tip {
font-size: 13px;
color: #333;
background: #fcd791;
padding: 10px;
}
</style>

View File

@ -1,298 +0,0 @@
<template>
<view class="pages">
<view class="placeholder-bar">
<statusBar></statusBar>
<view :style="{ height: `${navBarHeight}px` }"></view>
</view>
<view class="nav-box">
<!-- #ifndef H5 -->
<statusBar></statusBar>
<!-- #endif -->
<view class="nav" :style="{ height: `${navBarHeight}px` }">
<!-- #ifdef MP -->
<view class="mp-button-left-placeholder" :style="{ width: `${mpButtonLeftPlaceholderSize}px` }"></view>
<!-- #endif -->
<!-- 搜索功能 -->
<view class="uni-search-box">
<uni-search-bar ref="searchBar" radius="100" cancelButton="none" disabled
:placeholder="inputPlaceholder"/>
<view class="cover-search-bar" @click="searchClick"></view>
</view>
<!-- #ifdef MP -->
<view class="mp-button-placeholder" :style="{ width: `${mpButtonPlaceholderSize}px` }"></view>
<!-- #endif -->
</view>
</view>
<unicloud-db ref='udb' v-slot:default="{ pagination, hasMore, loading, error, options }" @error="onqueryerror"
:collection="colList" :page-size="10" orderby="publish_date desc" @load="listLoad">
<!-- 基于 uni-list 的页面布局 field="user_id.nickname"-->
<!-- #ifdef APP-NVUE -->
<list class="uni-list" :border="false" :style="{ height: listHeight }">
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<scroll-view
scroll-y
class="uni-list"
refresher-enabled
:refresher-triggered="loadType=== 'refresh'"
:style="{ height: listHeight }"
@refresherrefresh="refresh"
@scrolltolower="loadMore"
>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<refresh-box :loading="loading" @refresh="refresh"></refresh-box>
<!-- #endif -->
<!-- 列表渲染 -->
<template v-for="item in listData">
<not-cover v-if="item.thumbnail && item.thumbnail.length === 0" :data="item"></not-cover>
<right-small-cover v-else-if="item.thumbnail && item.thumbnail.length === 1"
:data="item"></right-small-cover>
<three-cover v-else-if="item.thumbnail && item.thumbnail.length === 3"
:data="item"></three-cover>
</template>
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
<!-- #ifdef APP-PLUS -->
<uni-list-item>
<template v-slot:body>
<!-- #endif -->
<uni-load-state @networkResume="refresh"
:state="{ data: listData, pagination, hasMore, loading, error }"
@loadMore="loadMore">
</uni-load-state>
<!-- #ifdef APP-PLUS -->
</template>
</uni-list-item>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
</list>
<!-- #endif -->
</unicloud-db>
</view>
</template>
<script>
import statusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar";
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
import refreshBox from "@/uni_modules/uni-cms-article/components/refresh-box/refreshBox.nvue";
import notCover from "@/uni_modules/uni-cms-article/components/list-template/not-cover.vue";
import rightSmallCover from "@/uni_modules/uni-cms-article/components/list-template/right-small-cover.vue";
import threeCover from "@/uni_modules/uni-cms-article/components/list-template/three-cover.vue";
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url";
const db = uniCloud.database();
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
export default {
components: {
statusBar,
refreshBox,
notCover,
rightSmallCover,
threeCover
},
computed: {
// 根据当前语言返回不同的搜索框占位符
inputPlaceholder(e) {
if (uni.getStorageSync('CURRENT_LANG') == "en") {
return 'Please enter the search content' // 英文
} else {
return '请输入搜索内容' // 中文
}
},
// 连表查询,返回两个集合的查询结果
colList() {
return [
db.collection(articleDBName).where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(), // 文章集合
db.collection(userDBName).field('_id,nickname').getTemp() // 用户集合
]
}
},
data() {
return {
where: '"article_status" == 1', // 查询条件
showRefresh: false, // 是否显示刷新按钮
listHeight: 0, // 列表高度
mpButtonLeftPlaceholderSize: 0, // 小程序左侧icon占位大小
mpButtonPlaceholderSize: 87, // 小程序导航栏按钮占位大小
navBarHeight: 44, // 导航栏高度
refreshStatus: 0, // 刷新状态 0: 未刷新 1: 刷新中 2: 刷新完成
listData: [], // 列表数据
loadType: null
}
},
async onReady() {
// #ifdef MP
this.initNavBarSize() // 初始化导航栏大小
// #endif
/* 可用窗口高度 - 搜索框高 - 状态栏高 */
this.listHeight = uni.getSystemInfoSync().windowHeight - uni.getSystemInfoSync().statusBarHeight - this.navBarHeight + 'px'; // 计算列表高度
},
methods: {
async listLoad(data) {
const listData = data.map(item => {
if (typeof item.thumbnail === 'string') {
item.thumbnail = [item.thumbnail]
}
return item
})
// 处理腾讯云文件链接
for (const article of listData) {
const parseImages = await parseImageUrl(article.thumbnail)
article.thumbnail = parseImages ? parseImages.map(image => image.src): []
}
this.listData = this.loadType === 'loadMore' ? this.listData.concat(listData) : listData
this.loadType = null
},
// 初始化导航栏大小
initNavBarSize() {
// 获取小程序导航栏按钮信息
// #ifdef MP-TOUTIAO
let menuButtonInfo = tt.getCustomButtonBoundingClientRect()
menuButtonInfo.width = menuButtonInfo.capsule.width // 小程序按钮区域中使用的按钮宽度
this.mpButtonLeftPlaceholderSize = menuButtonInfo.leftIcon.width + 10
// #endif
// #ifndef MP-TOUTIAO
let menuButtonInfo = uni.getMenuButtonBoundingClientRect()
// #endif
// 计算小程序导航栏按钮占位大小
this.mpButtonPlaceholderSize = menuButtonInfo.width + 10
// 获取系统信息,判断是否为 iOS 系统,设置导航栏高度
this.navBarHeight = uni.getSystemInfoSync().system.toLowerCase().includes('ios') ? 44 : 48
},
// 格式化时间戳
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
// 点击搜索框
searchClick(e) {
uni.hideKeyboard();
uni.navigateTo({
url: '/uni_modules/uni-cms-article/pages/search/search'
});
},
// 重试
retry() {
this.refresh()
},
// 刷新
refresh() {
this.loadType = 'refresh'
this.$refs.udb.loadData({
clear: true
}, () => {
uni.stopPullDownRefresh()
// #ifdef APP-NVUE
this.showRefresh = false
// #endif
})
},
// 加载更多
loadMore() {
this.loadType = 'loadMore'
this.$refs.udb.loadMore()
},
// 查询出错
onqueryerror(e) {
console.error(e);
}
},
// #ifdef H5
// 下拉刷新
onPullDownRefresh() {
this.refresh()
},
// #endif
}
</script>
<style scoped>
/* #ifndef APP-NVUE */
.pages view {
display: flex;
box-sizing: border-box;
flex-direction: column;
}
/* #endif */
.pages {
background-color: #FFFFFF;
}
.refresh {
text-align: center;
}
.nav-box {
background-color: #FFFFFF;
position: fixed;
top: 0;
left: 0;
right: 0;
/* #ifndef APP-PLUS */
z-index: 9;
/* #endif */
}
.pages .nav {
display: flex;
align-items: center;
flex-direction: row;
}
.uni-search-box {
flex: 1;
padding: 0 10px;
}
.uni-search-box ::v-deep .uni-searchbar {
padding: 0;
}
.uni-search-box ::v-deep .uni-searchbar__box {
height: 32px;
flex-direction: row;
}
.cover-search-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.pages .uni-list ::v-deep .uni-list-item__container {
flex-direction: row;
width: 100%;
}
.pages .uni-list ::v-deep .uni-list-item {
align-items: flex-start;
}
.pages .uni-list ::v-deep .uni-load-more {
display: flex;
}
.pages .uni-list ::v-deep .uni-list--border:after {
background-color: #f5f5f5;
}
</style>

View File

@ -1,33 +0,0 @@
<template>
<view class="page">
<uni-cms-article-search-bar :show-placeholder="true"></uni-cms-article-search-bar>
<uni-cms-article-list
:collectionList="colList"
:refresherEnabled="true"
style="flex: 1;"
></uni-cms-article-list>
</view>
</template>
<script lang="uts">
const db = uniCloud.databaseForJQL()
export default {
computed: {
colList(): any[] {
return [
db.collection('uni-cms-articles').where("\"article_status\" == 1").field('thumbnail,title,publish_date,user_id').getTemp(), // 文章集合
db.collection('uni-id-users').field('_id,nickname').getTemp() // 用户集合
]
}
}
}
</script>
<style scoped lang="scss">
.page {
display: flex;
flex-direction: column;
height: 100%;
}
</style>

View File

@ -1,752 +0,0 @@
<template>
<view class="container">
<view class="search-container">
<!-- 搜索框 -->
<view class="search-container-bar">
<!-- #ifdef APP-PLUS -->
<uni-icons class="search-icons" :color="iconColor" size="22" type="mic-filled" @click="speech" />
<!-- #endif -->
<!-- :cancelText="keyBoardPopup ? '取消' : '搜索'" -->
<uni-search-bar ref="searchBar" style="flex:1;" radius="100" v-model="searchText" :focus="focus"
:placeholder="hotWorld" clearButton="auto" cancelButton="none" @clear="cancel" @confirm="confirm"
@cancel="cancel" />
<uni-icons class="scan-icons" :color="iconColor" size="22" type="scan" @click="scanEvent"></uni-icons>
</view>
</view>
<view class="search-body">
<unicloud-db ref='listUdb' v-slot:default="{ pagination, hasMore, loading, error, options }"
@error="onqueryerror" :collection="colList" :page-size="10" orderby="publish_date desc" @load="onDbLoad"
loadtime="manual">
<template v-if="!isLoadData">
<!-- 搜索历史 -->
<view class="word-container" v-if="localSearchList.length">
<view class="word-container_header">
<text class="word-container_header-text">搜索历史</text>
<uni-icons v-if="!localSearchListDel" @click="localSearchListDel = true" class="search-icons"
style="padding-right: 0;" :color="iconColor" size="18" type="trash"></uni-icons>
<view v-else class="flex-center flex-row"
style="font-weight: 500;justify-content: space-between;">
<text
style="font-size: 22rpx;color: #666;padding-top:4rpx;padding-bottom:4rpx;padding-right:20rpx;"
@click="LocalSearchListClear">全部删除</text>
<text
style="font-size: 22rpx;color: #c0402b;padding-top:4rpx;padding-bottom:4rpx;padding-left:20rpx;"
@click="localSearchListDel = false">完成</text>
</view>
</view>
<view class="word-container_body">
<view class="flex-center flex-row word-container_body-text"
v-for="(word, index) in localSearchList" :key="index"
@click="LocalSearchlistItemClick(word, index)">
<text class="word-display" :key="word">{{ word }}</text>
<uni-icons v-if="localSearchListDel" size="12" type="closeempty" />
</view>
</view>
</view>
<!-- 搜索发现 -->
<view class="word-container">
<view class="word-container_header">
<view class="flex-center flex-row">
<text class="word-container_header-text">搜索发现</text>
<uni-icons v-if="!netHotListIsHide" class="search-icons" :color="iconColor" size="14"
type="reload" @click="searchHotRefresh"></uni-icons>
</view>
<uni-icons class="search-icons" style="padding-right: 0;" :color="iconColor" size="18"
:type="netHotListIsHide ? 'eye-slash' : 'eye'"
@click="netHotListIsHide = !netHotListIsHide"></uni-icons>
</view>
<unicloud-db ref="udb" #default="{ data, loading, error, options }" field="content"
collection="opendb-search-hot" orderby="create_date desc,count desc" page-data="replace"
:page-size="10">
<text v-if="loading && !netHotListIsHide" class="word-container_body-info">正在加载...</text>
<view v-else class="word-container_body">
<template v-if="!netHotListIsHide">
<text v-if="error" class="word-container_body-info">{{ error.message }}</text>
<template v-else>
<text v-for="(word, index) in data" class="word-container_body-text" :key="index"
@click="search(word.content)">{{ word.content }}</text>
</template>
</template>
<view v-else style="flex:1;">
<text class="word-container_body-info">当前搜索发现已隐藏</text>
</view>
</view>
</unicloud-db>
</view>
</template>
<uni-list v-else class="uni-list" :border="false" :style="{ height: listHeight }">
<!-- 列表渲染 -->
<uni-list-item :to="'/uni_modules/uni-cms-article/pages/detail/detail?id=' + item._id"
v-for="(item, index) in searchList" :key="index">
<!-- 通过header插槽定义列表左侧图片 -->
<template v-slot:header>
<image class="thumbnail" :src="item.thumbnail" mode="aspectFill"></image>
</template>
<!-- 通过body插槽定义布局 -->
<template v-slot:body>
<view class="main">
<text class="title">{{ item.title }}</text>
<view class="info">
<text class="author">{{ item.user_id[0] ? item.user_id[0].nickname : '' }}</text>
<text class="publish_date">{{ publishTime(item.publish_date) }}</text>
<!-- -->
<!-- <uni-dateformat class="publish_date" :date="item.publish_date"-->
<!-- format="yyyy-MM-dd" :threshold="[60000, 2592000000]"/>-->
</view>
</view>
</template>
</uni-list-item>
<!-- 加载状态:上拉加载更多,加载中,没有更多数据了,加载错误 -->
<!-- #ifdef APP-PLUS -->
<uni-list-item>
<template v-slot:body>
<!-- #endif -->
<uni-load-state @networkResume="refresh" :state="{ data: searchList, pagination, hasMore, loading, error }"
@loadMore="loadMore">
</uni-load-state>
<!-- #ifdef APP-PLUS -->
</template>
</uni-list-item>
<!-- #endif -->
</uni-list>
</unicloud-db>
</view>
<!-- 搜索联想 -->
<view class="search-associative" v-if="associativeShow">
<uni-list>
<uni-list-item v-for="(item, index) in associativeList" :key="item._id" :ellipsis="1" :title="item.title"
@click="associativeClick(item)" show-extra-icon clickable
:extra-icon="{ size: 18, color: iconColor, type: 'search' }">
</uni-list-item>
</uni-list>
</view>
</view>
</template>
<script>
/**
* 云端一体搜索模板
* @description uniCloud云端一体搜索模板自带下拉候选、历史搜索、热搜。无需再开发服务器代码
*/
import translatePublishTime from "@/uni_modules/uni-cms-article/common/publish-time";
import parseScanResult from "@/uni_modules/uni-cms-article/common/parse-scan-result";
import {parseImageUrl} from "@/uni_modules/uni-cms-article/common/parse-image-url";
const searchLogDbName = 'opendb-search-log'; // 搜索记录数据库
const articleDbName = 'uni-cms-articles'; // 文章数据库
const associativeSearchField = 'title'; // 联想时,搜索框值检索数据库字段名
const associativeField = '_id,title'; // 联想列表每一项携带的字段
const localSearchListKey = '__local_search_history'; // 本地历史存储字段名
const db = uniCloud.database();
const articleDBName = 'uni-cms-articles'
const userDBName = 'uni-id-users'
// 数组去重
const arrUnique = arr => {
for (let i = arr.length - 1; i >= 0; i--) {
const curIndex = arr.indexOf(arr[i]);
const lastIndex = arr.lastIndexOf(arr[i])
curIndex != lastIndex && arr.splice(lastIndex, 1)
}
return arr
} // 节流
// 防抖
function debounce(fn, interval, isFirstAutoRun) {
/**
*
* @param {要执行的函数} fn
* @param {在操作多长时间后可再执行,第一次立即执行} interval
*/
var _self = fn;
var timer = null;
var first = true;
if (isFirstAutoRun) {
_self();
}
return function () {
var args = arguments;
var _me = this;
if (first) {
first = false;
_self.apply(_me, args);
}
if (timer) {
clearTimeout(timer)
// return false;
}
timer = setTimeout(function () {
clearTimeout(timer);
timer = null;
_self.apply(_me, args);
}, interval || 200);
}
}
export default {
// 组件数据
data() {
return {
// 文章数据库名称
articleDbName,
// 搜索记录数据库名称
searchLogDbName,
// 状态栏高度
statusBarHeight: '0px',
// 本地搜索列表
localSearchList: uni.getStorageSync(localSearchListKey),
// 是否删除本地搜索列表
localSearchListDel: false,
// 是否隐藏网络热搜列表
netHotListIsHide: false,
// 搜索文本
searchText: '',
// 图标颜色
iconColor: '#999999',
// 联想列表
associativeList: [],
// 是否弹出键盘
keyBoardPopup: false,
// 搜索热词
hotWorld: 'DCloud', // 搜索热词,如果没有输入即回车,则搜索热词,但是不会加入搜索记录
// 是否自动聚焦
focus: true,
// 语音识别引擎
speechEngine: 'iFly', // 语音识别引擎 iFly 讯飞 baidu 百度
// 是否正在加载数据
isLoadData: false,
// 数据库查询条件
where: '"article_status" == 1',
// 列表高度
listHeight: 0,
// 是否显示联想列表
associativeShow: false,
// 是否显示无联想列表
noAssociativeShow: false,
// 搜索结果列表
searchList: []
}
},
// 组件创建时执行
created() {
// 初始化数据库
this.db = uniCloud.database();
this.searchLogDb = this.db.collection(this.searchLogDbName);
this.articleDbName = this.db.collection(this.articleDbName);
// #ifndef H5
// 监听键盘高度变化
uni.onKeyboardHeightChange((res) => {
this.keyBoardPopup = res.height !== 0;
})
// #endif
},
// 计算属性
computed: {
colList() {
// 返回文章和用户列表
return [
db.collection(articleDBName).where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(),
db.collection(userDBName).field('_id,nickname').getTemp()
]
}
},
// 页面初次渲染完成时执行
onReady() {
// #ifdef APP-NVUE
/* 可用窗口高度 - 搜索框高 - 状态栏高 */
this.listHeight = uni.getSystemInfoSync().windowHeight + 'px';
// #endif
// #ifndef APP-NVUE
this.listHeight = 'auto'
// #endif
},
// 页面加载时执行
onLoad() {
//#ifdef APP-PLUS
// 获取状态栏高度
this.statusBarHeight = `${uni.getSystemInfoSync().statusBarHeight}px`;
//#endif
},
// 组件方法
methods: {
// 清空搜索框
clear(res) {
console.log("res: ", res);
},
// 确认搜索
confirm(res) {
// 键盘确认
this.search(res.value);
},
// 取消搜索
cancel(res) {
uni.hideKeyboard();
this.searchText = '';
this.isLoadData = false
this.associativeShow = false
// this.loadList();
},
// 执行搜索
search(value) {
if (!value && !this.hotWorld) {
return;
}
if (value) {
if (this.searchText !== value) {
this.searchText = value
}
this.localSearchListManage(value);
this.searchLogDbAdd(value)
} else if (this.hotWorld) {
this.searchText = this.hotWorld
}
uni.hideKeyboard();
this.loadList(this.searchText);
},
// 管理本地搜索列表
localSearchListManage(word) {
let list = uni.getStorageSync(localSearchListKey);
if (list.length) {
this.localSearchList.unshift(word);
arrUnique(this.localSearchList);
if (this.localSearchList.length > 10) {
this.localSearchList.pop();
}
} else {
this.localSearchList = [word];
}
uni.setStorageSync(localSearchListKey, this.localSearchList);
},
// 清空本地搜索列表
LocalSearchListClear() {
uni.showModal({
content: "确认清空搜索历史吗",
confirmText: "删除",
confirmColor: 'red',
cancelColor: '#808080',
success: res => {
if (res.confirm) {
this.localSearchListDel = false;
this.localSearchList = [];
uni.removeStorageSync(localSearchListKey)
}
}
});
},
// 点击本地搜索列表项
LocalSearchlistItemClick(word, index) {
if (this.localSearchListDel) {
this.localSearchList.splice(index, 1);
uni.setStorageSync(localSearchListKey, this.localSearchList);
if (!this.localSearchList.length) {
this.localSearchListDel = false;
}
return;
}
this.noAssociativeShow = true;
this.search(word);
},
// 刷新搜索热词
searchHotRefresh() {
this.$refs.udb.refresh();
},
// 语音搜索
speech() {
// #ifdef APP-PLUS
plus.speech.startRecognize({
engine: this.speechEngine,
punctuation: false, // 标点符号
timeout: 10000
}, word => {
word = word instanceof Array ? word[0] : word;
this.search(word)
}, err => {
console.error("语音识别错误: ", err);
});
// #endif
},
// 添加搜索记录
searchLogDbAdd(value) {
/*
在此处存搜索记录,如果登录则需要存 user_id若未登录则存device_id
*/
this.getDeviceId().then(device_id => {
this.searchLogDb.add({
// user_id: device_id,
device_id,
content: value,
create_date: Date.now()
})
})
},
// 获取设备ID
getDeviceId() {
return new Promise((resolve, reject) => {
// 从本地缓存中获取uni_id
const uniId = uni.getStorageSync('uni_id');
// 如果uni_id不存在则获取设备信息
if (!uniId) {
// #ifdef APP-PLUS
plus.device.getInfo({
success: (deviceInfo) => {
resolve(deviceInfo.uuid)
},
fail: () => {
// 如果获取设备信息失败,则返回一个随机字符串
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
}
});
// #endif
// #ifndef APP-PLUS
// 如果不是APP-PLUS则返回一个随机字符串
resolve(uni.getSystemInfoSync().system + '_' + Math.random().toString(36).substr(2))
// #endif
} else {
// 如果uni_id存在则直接返回uni_id
resolve(uniId)
}
})
},
// 点击联想词
associativeClick(item) {
/**
* 注意:这里用户根据自己的业务需要,选择跳转的页面即可
*/
console.log("associativeClick: ", item, item.title);
// 隐藏联想词
this.noAssociativeShow = true;
// 将搜索框的文本设置为联想词的标题
this.searchText = item.title;
// 加载列表
this.loadList(item.title);
},
// 加载列表
loadList(text = '') {
// 设置查询条件
let where = '"article_status" == 1 '
if (text) {
this.where = where + `&& /${text}/.test(title)`;
} else {
this.where = where;
}
// 隐藏联想词
this.associativeList = [];
this.associativeShow = false;
this.searchList = []
this.isLoadData = true
// 延迟0ms后加载数据
setTimeout(() => {
this.$refs.listUdb.loadData({
clear: true
})
}, 0)
},
// 数据库加载完成
async onDbLoad(data) {
console.log('onDbLoad')
// 设置数据已加载标志
this.isLoadData = true
// 显示联想词
this.noAssociativeShow = false;
for (const article of data) {
const parseImages = await parseImageUrl(article.thumbnail)
article.thumbnail = parseImages ? parseImages.map(image => image.src): []
}
this.searchList = data
},
// 查询错误
onqueryerror(e) {
console.error(e);
},
// 刷新
refresh() {
// 刷新数据
this.$refs.listUdb.loadData({
clear: true
}, () => {
// 停止下拉刷新
uni.stopPullDownRefresh()
// #ifdef APP-NVUE
// 隐藏刷新按钮
this.showRefresh = false
// #endif
})
},
// 加载更多
loadMore() {
// 加载更多数据
this.$refs.listUdb.loadMore()
},
// 格式化发布时间
publishTime(timestamp) {
return translatePublishTime(timestamp)
},
scanEvent () {
uni.scanCode({
onlyFromCamera: true,
scanType: ["qrCode"],
success: (e) => parseScanResult(e.result),
fail: (e) => {
console.error(e)
}
})
}
},
onReachBottom() {
// 当滚动到底部时,加载更多数据
this.loadMore()
},
watch: {
searchText: debounce(function (value, oldValue) {
// 当搜索框的文本发生变化时,执行以下操作
if (value === oldValue) return
if (this.noAssociativeShow) return
if (value) {
// 根据搜索框的文本,查询联想词
this.articleDbName.where({
[associativeSearchField]: new RegExp(value, 'gi'),
}).field(associativeField).get().then(res => {
// 将查询结果赋值给联想词列表,并显示联想词
this.associativeList = res.result.data;
this.associativeShow = true
})
} else {
// 如果搜索框的文本为空,则清空联想词列表
this.associativeList = [];
}
}, 100)
}
}
</script>
<style>
/* #ifndef APP-NVUE */
page {
height: 100%;
flex: 1;
}
/* #endif */
</style>
<style lang="scss" scoped>
$search-bar-height: 52px;
$word-container_header-height: 72rpx;
.status-bar {
background-color: #fff;
}
.container {
/* #ifndef APP-NVUE */
height: 100%;
/* #endif */
flex: 1;
background-color: #f7f7f7;
}
.search-body {
background-color: #fff;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
}
@mixin uni-flex {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
}
@mixin words-display {
font-size: 26rpx;
color: #666;
}
.flex-center {
@include uni-flex;
justify-content: center;
align-items: center;
}
.flex-row {
@include uni-flex;
flex-direction: row;
}
/* #ifdef APP-PLUS */
/* #ifndef APP-NVUE || VUE3*/
::v-deep
/* #endif */
.uni-searchbar {
padding-left: 0;
}
/* #endif */
/* #ifndef APP-NVUE || VUE3*/
::v-deep
/* #endif */
.uni-searchbar__box {
border-width: 0;
}
/* #ifndef APP-NVUE || VUE3 */
::v-deep
/* #endif */
.uni-input-placeholder {
font-size: 28rpx;
}
.search-container {
height: $search-bar-height;
@include uni-flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
background-color: #fff;
@at-root {
#{&}-bar {
@include uni-flex;
flex-direction: row;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
}
.search-associative {
/* #ifndef APP-NVUE */
overflow-y: auto;
/* #endif */
position: absolute;
top: $search-bar-height;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
margin-top: 10rpx;
padding-left: 10rpx;
padding-right: 10rpx;
}
.search-icons, .scan-icons {
padding: 16rpx;
}
.scan-icons {
padding-left: 0;
}
.word-display {
@include words-display;
}
.word-container {
padding: 20rpx;
@at-root {
#{&}_header {
@include uni-flex;
height: $word-container_header-height;
line-height: $word-container_header-height;
flex-direction: row;
justify-content: space-between;
align-items: center;
@at-root {
#{&}-text {
color: #3e3e3e;
font-size: 30rpx;
font-weight: bold;
}
}
}
#{&}_body {
@include uni-flex;
flex-wrap: wrap;
flex-direction: row;
@at-root {
#{&}-text {
@include uni-flex;
@include words-display;
justify-content: center;
align-items: center;
background-color: #f6f6f6;
padding: 10rpx 20rpx;
margin: 20rpx 30rpx 0 0;
border-radius: 30rpx;
/* #ifndef APP-NVUE */
box-sizing: border-box;
/* #endif */
text-align: center;
}
#{&}-info {
/* #ifndef APP-NVUE */
display: block;
/* #endif */
flex: 1;
text-align: center;
font-size: 26rpx;
color: #808080;
margin-top: 20rpx;
}
}
}
}
}
.thumbnail {
width: 240rpx;
height: 160rpx;
margin-right: 20rpx;
border-radius: 8rpx;
}
.main {
justify-content: space-between;
flex: 1;
}
.title {
font-size: 32rpx;
}
.info {
flex-direction: row;
justify-content: space-between;
}
.author,
.publish_date {
font-size: 28rpx;
color: #999999;
}
</style>

View File

@ -1,363 +0,0 @@
<template>
<view class="container">
<view class="search">
<uni-cms-article-search-bar
ref="searchBar"
v-model="searchVal"
:show-placeholder="false"
:focus="true"
@clear="showSearchResultPanel = false"
@confirm="search"
></uni-cms-article-search-bar>
</view>
<view class="search-result" v-if="showSearchResultPanel">
<uni-cms-article-list
ref="articleList"
:collectionList="colList"
:refresherEnabled="false"
loadTime="manual"
style="flex: 1;"
></uni-cms-article-list>
</view>
<template v-else>
<view class="panel history-panel" v-if="searchHistory.length > 0">
<view class="panel__title">
<view class="panel__title-text">
<text class="text">搜索历史</text>
</view>
<view class="delete-history-btns" v-if="deleteHistoryLoading">
<text class="text" @click="deleteAllSearchHistory">全部删除</text>
<text class="text danger" @click="deleteHistoryLoading = false">完成</text>
</view>
<uni-cms-article-icons
class="panel__after-icon"
type="trash"
:size="18"
color="#999"
@click="deleteHistoryLoading = true"
v-else
></uni-cms-article-icons>
</view>
<view class="panel__list">
<view class="panel__list-item" v-for="text in searchHistory">
<text class="text" @click="search(text)">{{text}}</text>
<uni-cms-article-icons
class="icon"
type="closeempty"
:size="12"
color="#999"
v-if="deleteHistoryLoading"
@click="deleteSearchHistory(text)"
></uni-cms-article-icons>
</view>
</view>
</view>
<unicloud-db ref="udb" #default="{ data, loading, error }" field="content"
collection="opendb-search-hot" orderby="create_date desc,count desc" page-data="replace"
:page-size="10">
<view class="panel recommend-panel">
<view class="panel__title">
<view class="panel__title-text">
<text class="text">搜索发现</text>
<uni-cms-article-icons
class="icon"
type="reload"
:size="14"
color="#999"
v-if="!hideSearchRecommend"
@click="reLoadSearchRecommend"
></uni-cms-article-icons>
</view>
<uni-cms-article-icons
class="panel__after-icon"
:type="hideSearchRecommend ? 'eye-slash': 'eye'"
:size="18"
color="#999"
@click="hideSearchRecommend = !hideSearchRecommend"
></uni-cms-article-icons>
</view>
<view class="panel__list">
<view class="panel__list-tip" v-if="loading">
<text class="text">正在加载...</text>
</view>
<view class="panel__list-tip" v-else-if="error != null">
<text class="text">{{error.message}}</text>
</view>
<view class="panel__list-tip" v-else-if="hideSearchRecommend">
<text class="text">当前搜索发现已隐藏</text>
</view>
<template v-else>
<view
class="panel__list-item"
v-for="(word, index) in data"
:key="index"
@click="search(word.getString('content')!)"
>
<text class="text">{{ word.getString('content') }}</text>
</view>
</template>
</view>
</view>
</unicloud-db>
</template>
</view>
</template>
<script lang="uts">
type ArticleAuthor = {
_id: string
nickname: string
}
type ArticleItem = {
_id: string
title: string
publish_date: number
thumbnail: string[]
user_id: ArticleAuthor[]
}
const db = uniCloud.databaseForJQL()
const searchLogDB = db.collection('opendb-search-log')
const cmsArticleDB = db.collection('uni-cms-articles')
const uniIdUsersDB = db.collection('uni-id-users')
const localSearchHistoryKey = '__local_search_history'; // 本地历史存储字段名
const localSearchRecommendHiddenKey = '__local_search_recommend_hidden'; // 本地搜索发现开关字段名
const localSearchHistoryMax = 10; // 本地历史存储最大值
export default {
data() {
const localSearchRecommendHidden = uni.getStorageSync(localSearchRecommendHiddenKey)
return {
searchVal: "",
searchHistory: [] as string[],
searchRecommend: [] as string[],
deleteHistoryLoading: false,
hideSearchRecommend: (localSearchRecommendHidden == "" ? false : localSearchRecommendHidden) as boolean,
showSearchResultPanel: false,
searchResult: [] as ArticleItem[]
}
},
watch: {
hideSearchRecommend(newValue) {
uni.setStorageSync(localSearchRecommendHiddenKey, newValue)
}
},
computed: {
hasSearchValue(): boolean {
return this.searchVal != ""
},
where(): string {
let where = "\"article_status\" == 1"
if (this.searchVal != "") {
where += `&& /${this.searchVal}/.test(title)`
}
return where
},
colList(): any[] {
// 返回文章和用户列表
return [
cmsArticleDB.where(this.where).field('thumbnail,title,publish_date,user_id').getTemp(),
uniIdUsersDB.field('_id,nickname').getTemp()
]
}
},
mounted() {
// 本地历史存储
const localSearchHistory = uni.getStorageSync(localSearchHistoryKey)
this.searchHistory = (localSearchHistory == "" ? [] as string[] : localSearchHistory) as string[]
},
methods: {
deleteAllSearchHistory() {
uni.showModal({
title: "确定清空搜索历史吗",
confirmText: "删除",
success: (res) => {
if (res.confirm) {
this.deleteSearchHistory(null)
}
}
})
},
deleteSearchHistory(searchText: string | null) {
let history: string[] = []
if (searchText != null) {
history = this.searchHistory.filter((item: string): boolean => item != searchText)
}
this.searchHistory = history
uni.setStorageSync(localSearchHistoryKey, history)
console.log(history.length, 'history.length')
if (history.length <= 0) {
this.deleteHistoryLoading = false
}
},
search(searchText: string) {
searchText = searchText.trim()
if (searchText == "" || this.deleteHistoryLoading) return
// 隐藏键盘
;(this.$refs['searchBar'] as UniCmsArticleSearchBarComponentPublicInstance)!.hideKeyboard()
// 保存搜索历史
this.setLocalSearchHistory(searchText)
// 显示搜索结果Panel
this.showSearchResultPanel = true
// 搜索
this.loadSearchResult(searchText)
// 添加搜索记录
this.addSearchRecord(searchText)
},
loadSearchResult(searchText: string) {
// 设置查询条件
this.searchVal = searchText
// 延迟0ms后加载数据
setTimeout(() => {
(this.$refs['articleList'] as UniCmsArticleListComponentPublicInstance)!.reLoadList()
}, 0)
},
addSearchRecord(searchText: string) {
const systemInfo = uni.getSystemInfoSync()
/*
在此处存搜索记录,如果登录则需要存 user_id若未登录则存device_id
*/
searchLogDB.add({
// user_id: device_id,
device_id: systemInfo.deviceId,
// device_uuid: systemInfo.deviceId,
content: searchText,
create_date: Date.now()
})
},
setLocalSearchHistory(searchText: string) {
const history = this.searchHistory.filter((item: string): boolean => item != searchText)
history.unshift(searchText)
if (history.length > localSearchHistoryMax) {
history.pop()
}
this.searchHistory = history
this.deleteHistoryLoading = false
uni.setStorageSync(localSearchHistoryKey, history)
},
reLoadSearchRecommend() {
(this.$refs['udb'] as UniCloudDBElement)!.loadData({
clear: true
})
}
}
}
</script>
<style lang="scss">
.container {
display: flex;
flex-direction: column;
height: 100%;
.search-result {
flex: 1;
display: flex;
flex-direction: column;
}
}
.panel {
margin-top: 40rpx;
padding: 10rpx 20rpx;
&__title {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
&-text {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
.text {
color: #3e3e3e;
font-size: 30rpx;
line-height: 36rpx;
font-weight: bold;
}
.icon {
margin-left: 10rpx;
}
}
}
&__after-icon {
margin-left: 10rpx;
}
&__list {
margin-top: 30rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
&-item {
background-color: #f6f6f6;
border-radius: 30rpx;
padding: 10rpx 20rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
margin-right: 10rpx;
margin-bottom: 10rpx;
.text {
font-size: 26rpx;
color: #666;
}
.icon {
margin-left: 10rpx;
}
}
&-tip {
display: flex;
flex-direction: row;
justify-content: center;
flex: 1;
margin-top: 20rpx;
.text {
font-size: 26rpx;
color: #808080;
}
}
}
}
.delete-history-btns {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.text {
color: #666;
font-size: 22rpx;
margin-left: 20rpx;
&.danger {
color: #c0402b;
}
}
}
</style>

View File

@ -1,48 +0,0 @@
<!-- 网络链接内容展示页用于uni-cms-article展示外链内容 -->
<template>
<view class="web-view">
<web-view
class="web-view"
v-if="url"
:src="url"
></web-view>
</view>
</template>
<script lang="uts">
export default {
onLoad(e) {
const url = e.get('url') as string
let title: string | null = e.get('title')
if (url.substring(0, 4) != 'http') {
uni.showModal({
title: "错误",
content: '不是一个有效的网站链接,' + '"' + decodeURIComponent(url) + '"',
showCancel: false,
confirmText: "知道了",
complete: () => {
uni.navigateBack()
}
})
title = "页面路径错误"
} else {
this.url = decodeURIComponent(url)
}
if (title != null) {
uni.setNavigationBarTitle({title})
}
},
data() {
return {
url: null as string | null,
}
}
}
</script>
<style>
.web-view {
height: 100%;
}
</style>

View File

@ -1,35 +0,0 @@
<!-- 网络链接内容展示页用于uni-cms-article展示外链内容 -->
<template>
<view>
<web-view v-if="url" :src="url"></web-view>
</view>
</template>
<script>
export default {
onLoad({url,title}) {
if(url.substring(0, 4) !== 'http'){
uni.showModal({
title:"错误",
content: '不是一个有效的网站链接,'+'"'+url+'"',
showCancel: false,
confirmText:"知道了",
complete: () => {
uni.navigateBack()
}
});
title = "页面路径错误"
}else{
this.url = url;
}
if(title){
uni.setNavigationBarTitle({title});
}
},
data() {
return {
url:null
};
}
}
</script>

View File

@ -1,24 +0,0 @@
# uni-cms-article
uni-CMS是基于uniCloud开发的uni-admin插件可用于快速搭建CMS内容管理系统。
客户端可使用 uni-cms-article 插件进行内容展示,无需开发,即可在多端展示内容。
![](https://web-assets.dcloud.net.cn/unidoc/zh/202304120145412.png)
![](https://web-assets.dcloud.net.cn/unidoc/zh/202304111812979.png)
<div style="display: flex; flex-basis: 10px">
<div style="margin-right: 10px;">
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120144625.png" width="375"/>
</div>
<div style="margin-right: 10px;">
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120139988.png" width="375" />
</div>
<div style="margin-right: 10px;">
<img src="https://web-assets.dcloud.net.cn/unidoc/zh/202304120139209.png" width="375" />
</div>
</div>
---
## 详细文档请在 [uni-cms 文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html) 中查看

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

File diff suppressed because one or more lines are too long

View File

@ -1,257 +0,0 @@
const {
QuillDeltaToHtmlConverter: QuillDeltaToHtmlConverterBase,
BlockGroup,
ListGroup,
BlotBlock,
} = require('./core')
const jsonRules = {
list(list) {
const firstItem = list.items[0];
return {
type: "list",
data: {
type: firstItem.item.op.attributes.list,
items: list.items.map((item) => jsonRules.listItem(item)),
},
attributes: firstItem.item.op.attributes,
class: attributes2class(firstItem.item.op.attributes, 'block'),
style: attributes2style(firstItem.item.op.attributes, 'block'),
};
},
listItem(listItem) {
// listItem.item.op.attributes.indent = 0;
const inlines = jsonRules.inlines(listItem.item.op, listItem.item.ops, false).map((v) => v.ops)
return {
data: inlines[0] || [],
children: listItem.innerList ? jsonRules.list(listItem.innerList): [],
}
},
block (op, ops) {
const type = ops[0].insert.type
if (type === 'text') {
return jsonRules.inlines(op, ops)
}
return {
type: ops[0].insert.type,
data: ops.map(bop => jsonRules.inline(bop)),
attributes: op.attributes,
class: attributes2class(op.attributes, 'block'),
style: attributes2style(op.attributes, 'block'),
}
},
inlines(op = {}, ops, isInlineGroup = true) {
const opsLen = ops.length - 1;
const br = {
type: 'br'
}
const texts = ops.reduce((acc, op, i) => {
if (i > 0 && i === opsLen && op.isJustNewline()) {
acc[acc.length - 1].op = op
return acc;
}
if (!acc[acc.length - 1]) {
acc.push({ops: [], op: {}});
}
if (op.isJustNewline()) {
const nextOp = ops[i + 1];
acc[acc.length - 1].op = op
if (nextOp && nextOp.isJustNewline()) {
acc.push({ops: [br], op: {}});
} else {
acc.push({ops: [], op: {}});
}
return acc;
} else {
acc[acc.length - 1].ops.push(jsonRules.inline(op));
return acc;
}
}, []);
if (!isInlineGroup) {
return texts;
}
return texts.map((v) => {
return {
type: "paragraph",
data: v.ops,
class: attributes2class(op.attributes || v.op.attributes, 'block'),
style: attributes2style(op.attributes || v.op.attributes, 'block'),
};
});
},
inline (op) {
const data = {
value: op.insert.value,
attributes: op.attributes,
class: attributes2class(op.attributes, 'inline'),
style: attributes2style(op.attributes, 'inline'),
}
if (op.isCustomEmbed()) {
return {
type: op.insert.type,
data
}
}
if (op.isLink()) {
return {
type: 'link',
data
}
}
return {
type: op.isImage() ? "image": "text",
data
}
},
blotBlock (op) {
return {
type: op.insert.type,
data: {
value: op.insert.value,
attributes: op.attributes,
class: attributes2class(op.attributes, 'block'),
style: attributes2style(op.attributes, 'block'),
}
}
}
};
function attributes2style(attributes, type) {
if (!attributes) return ''
// 定义允许的属性
const allowAttr = {
inline: ['align', 'color', 'background', 'font', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'fontFamily', 'lineHeight', 'letterSpacing', 'textDecoration', 'textIndent', 'wordWrap', 'wordBreak', 'whiteSpace'],
block: ['align', 'margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight', 'padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'lineHeight', 'textIndent']
}[type]
// 定义属性适配器
const adp = {
align: 'text-align',
}
// 如果不支持该类型的属性,则抛出错误
if (!allowAttr) throw new Error('type not supported')
// 将属性转换为style
return allowAttr.reduce((res, item) => {
// 如果属性为空,则返回空字符串
if (!attributes) return res
// 如果属性不为undefined则将属性名和属性值添加到style列表中
if (attributes[item] !== undefined) {
// 如果属性适配器中存在该属性,则使用适配器中的属性名
// 否则,将属性名转换为短横线连接的形式
res.push(`${adp[item] || item.replace(/([A-Z])/g, (s, m) => `-${m.toLowerCase()}`)}: ${attributes[item]}`)
}
return res
}, []).join(';')
}
function attributes2class(attributes, type) {
if (!attributes) return ''
// 定义允许的属性
const allowAttr = {
inline: ['bold', 'italic', 'underline', 'strike', 'ins', 'link'],
block: ['header', 'list']
}[type]
// 如果不支持该类型的属性,则抛出错误
if (!allowAttr) throw new Error('type not supported')
// 将属性转换为class列表
const classList = allowAttr.reduce((res, item) => {
// 如果属性为空,则返回空字符串
if (!attributes || item === "link") return res
// 如果属性为true则将属性名添加到class列表中
if (attributes[item] === true) {
res.push(item)
// 如果属性不为undefined则将属性名和属性值添加到class列表中
} else if (attributes[item] !== undefined) {
res.push(`${item}-${attributes[item]}`)
}
return res
}, [])
// 如果属性中包含link则添加link类
if ('link' in attributes) {
classList.push('link')
}
return classList.join(' ')
}
class QuillDeltaToJSONConverter {
constructor(deltaOps) {
this.deltaPreHandler(deltaOps)
this.deltaOps = deltaOps
this.converter = new QuillDeltaToHtmlConverterBase(this.deltaOps, {
multiLineParagraph: false,
});
}
deltaPreHandler (deltaOps) {
for (const op of deltaOps) {
if (typeof op.insert === 'string') continue
if (!op.attributes) {
op.attributes = {}
}
const insertType = Object.keys(op.insert)
const blockRenderList = ['divider', 'unlockContent', 'mediaVideo']
if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
op.attributes.renderAsBlock = true
}
}
}
convert () {
const opsGroups = this.converter.getGroupedOps();
return [].concat.apply(
[],
opsGroups.map((group) => {
// console.log(JSON.stringify(group), '--------------------group------------------------------')
switch (group.constructor) {
case ListGroup:
return jsonRules.list(group);
case BlockGroup:
return jsonRules.block(group.op, group.ops)
case BlotBlock:
return jsonRules.blotBlock(group.op, group.ops)
default:
return jsonRules.inlines(group.op, group.ops);
}
})
).filter(op => op.data instanceof Array ? op.data.length : op.data);
}
}
class QuillDeltaToHtmlConverter extends QuillDeltaToHtmlConverterBase {
constructor(deltaOps) {
super(deltaOps, {
multiLineParagraph: false,
inlineStyles: true,
});
// this.renderCustomWith(function (customOp, contextOp) {
// console.log(customOp, contextOp)
// })
}
}
exports.QuillDeltaToHtmlConverter = QuillDeltaToHtmlConverter
exports.QuillDeltaToJSONConverter = QuillDeltaToJSONConverter

View File

@ -1,5 +0,0 @@
{
"name": "quill-delta-converter",
"description": "quill编辑器delta格式转换器",
"main": "index.js"
}

View File

@ -1,58 +0,0 @@
const crypto = require('crypto')
const createConfig = require('uni-config-center')
const config = createConfig({
pluginId: 'uni-cms'
}).config()
const unlockRecordDBName = 'uni-cms-unlock-record'
// 定义云函数
exports.main = async function (event) {
// 解构 event 对象
const {trans_id, extra: _extra, sign} = event
let extra = {}
try {
extra = JSON.parse(_extra)
} catch (e) {}
// 如果 adConfig 或 securityKey 配置项不存在,则抛出错误引导用户配置参数
if (!config.adConfig || !config.adConfig.securityKey) throw new Error('请先配置adConfig.securityKey')
// 如果 extra.article_id 不存在,则返回 null
if (!extra.article_id) return null
// 签名验证
const reSign = crypto.createHash('sha256').update(`${config.adConfig.securityKey}:${trans_id}`).digest('hex')
if (sign !== reSign) {
console.log('签名错误', `${config.adConfig.securityKey}:${trans_id}`)
return null
}
// 获取数据库实例
const db = uniCloud.database()
// 查询解锁记录
const unlockRecord = await db.collection(unlockRecordDBName).where({
trans_id
}).get()
// 如果已经解锁过了,则返回 null
if (unlockRecord.data.length) {
console.log('已经解锁过了')
return null // 已经解锁过了
}
// 添加解锁记录
await db.collection(unlockRecordDBName).add({
unique_id: extra.unique_id,
unique_type: extra.unique_type,
article_id: extra.article_id,
trans_id,
create_date: Date.now()
})
console.log('解锁成功')
// 应广告规范,需返回 isValid 为 true
return {
isValid: true
}
}

View File

@ -1,7 +0,0 @@
{
"name": "uni-cms-unlock-callback",
"dependencies": {
"uni-config-center": "file:../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
},
"extensions": {}
}

View File

@ -1,23 +0,0 @@
{
"uni-cms-unlock-record": {
"data": [],
"index": [
{
"IndexName": "unique_article_trans_",
"MgoKeySchema": {
"MgoIndexKeys": [{
"Name": "unique_id",
"Direction": "1"
},{
"Name": "article_id",
"Direction": "1"
},{
"Name": "trans_id",
"Direction": "1"
}],
"MgoIsUnique": false
}
}
]
}
}

View File

@ -1,31 +0,0 @@
{
"bsonType": "object",
"required": [
"content",
"count"
],
"permission": {
"read": true,
"create": false,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID系统自动生成"
},
"content": {
"bsonType": "string",
"description": "搜索内容"
},
"count": {
"bsonType": "int",
"description": "搜索次数"
},
"create_date": {
"bsonType": "timestamp",
"description": "统计时间"
}
},
"version": "0.0.1"
}

View File

@ -1,45 +0,0 @@
{
"bsonType": "object",
"required": [
"content"
],
"permission": {
"read": false,
"create": true,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "搜索人id参考uni-id-users表"
},
"device_id": {
"bsonType": "string",
"description": "设备id"
},
"platform": {
"bsonType": "string",
"description": "设备平台mp-weixin、app-plus等"
},
"content": {
"bsonType": "string",
"description": "搜索内容"
},
"ip": {
"bsonType": "string",
"description": "客户端IP地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"create_date": {
"bsonType": "timestamp",
"description": "统计时间"
}
},
"version": "0.0.1"
}

View File

@ -1,45 +0,0 @@
{
"bsonType": "object",
"required": [
"content"
],
"permission": {
"read": false,
"create": true,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "ID系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "搜索人id参考uni-id-users表"
},
"device_id": {
"bsonType": "string",
"description": "设备id"
},
"platform": {
"bsonType": "string",
"description": "设备平台mp-weixin、app-plus等"
},
"content": {
"bsonType": "string",
"description": "搜索内容"
},
"ip": {
"bsonType": "string",
"description": "客户端IP地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"create_date": {
"bsonType": "timestamp",
"description": "统计时间"
}
},
"version": "0.0.1"
}

View File

@ -1,350 +0,0 @@
// 获取配置
const createConfig = safeRequire('uni-config-center')
const {QuillDeltaToHtmlConverter, QuillDeltaToJSONConverter} = safeRequire('quill-delta-converter')
const config = createConfig({
pluginId: 'uni-cms'
}).config()
// 获取数据库实例
const db = uniCloud.database()
// 文章数据库名称
const articleDBName = 'uni-cms-articles'
// 解锁内容数据库名称
const unlockContentDBName = 'uni-cms-unlock-record'
// 安全检测文本内容
async function checkContentSec(content, requestId, errorMsg) {
// 安全引入内容安全检测模块
const UniSecCheck = safeRequire('uni-sec-check')
// 创建内容安全检测实例
const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
requestId
})
// 调用文本安全检测接口
const res = await uniSecCheck.textSecCheck({
content, // 待检测的文本内容
scene: 1, // 表示资料类场景
version: 1 // 调用检测API的版本号
})
// 如果存在敏感词,抛出异常
if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
throw new Error(errorMsg || '存在敏感词,请修改后提交')
} else if (res.errCode !== 0) {
console.error(res)
throw new Error('内容安全检测异常:' + res.errCode)
}
}
// 安全检测图片内容
async function checkImageSec(image, requestId, errorMsg) {
// 安全引入内容安全检测模块
const UniSecCheck = safeRequire('uni-sec-check')
// 创建内容安全检测实例
const uniSecCheck = new UniSecCheck({
provider: 'mp-weixin',
requestId
})
const images = typeof image === "string" ? [image]: image
for (let item of images) {
// 处理cloud://开头的链接
if (item.startsWith('cloud://')) {
const res = await uniCloud.getTempFileURL({
fileList: [item]
})
if (res.fileList && res.fileList.length > 0) {
item = res.fileList[0].tempFileURL
}
}
// 调用图片安全检测接口
const res = await uniSecCheck.imgSecCheck({
image: item, // 待检测的图片URL
scene: 1, // 表示资料类场景
version: 1 // 调用检测API的版本号
})
// 如果存在违规内容,抛出异常
if (res.errCode === uniSecCheck.ErrorCode.RISK_CONTENT) {
throw new Error(errorMsg || '图片违规,请修改后提交')
} else if (res.errCode !== 0) {
console.error(res)
throw new Error('内容安全检测异常:' + res.errCode)
}
}
}
// 检测内容安全开关
function checkContentSecurityEnable(field) {
// 1. 从配置中心获取配置
return config.contentSecurity && config.contentSecurity.allowCheckType && config.contentSecurity.allowCheckType.includes(field)
}
// 安全require
function safeRequire(module) {
try {
return require(module)
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
throw new Error(`${module} 公共模块不存在,请在 uniCloud/database 目录右击"配置schema扩展公共模块"添加 ${module} 模块`)
}
}
}
module.exports = {
trigger: {
// 创建文章前触发
beforeCreate: async function ({clientInfo, addDataList}) {
// addDataList 是一个数组,因为可以一次性创建多条数据
if (addDataList.length <= 0) return
// 检测内容安全开关
const allowCheckContent = checkContentSecurityEnable('content')
const allowCheckImage = checkContentSecurityEnable('image')
// 遍历数组,对每一条数据进行安全检测
for (const addData of addDataList) {
// 如果是草稿,不检测
if (addData.article_status !== 1) continue
// 并行检测
const parallel = []
// 检测标题
if (allowCheckContent && addData.title) {
parallel.push(checkContentSec(addData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
}
// 检测摘要
if (allowCheckContent && addData.excerpt) {
parallel.push(checkContentSec(addData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
}
// 检测内容
if (allowCheckContent && addData.content) {
parallel.push(checkContentSec(JSON.stringify(addData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
}
// 检测封面图
if (allowCheckImage && addData.thumbnail) {
parallel.push(checkImageSec(addData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
}
// 等待所有并行检测完成
await Promise.all(parallel)
}
},
// 更新文章前触发
beforeUpdate: async function ({clientInfo, where, updateData}) {
const id = where && where._id
if (!id) return
// 如果是草稿,不检测
if (updateData.article_status !== 1) return
// 检测内容安全开关
const allowCheckContent = checkContentSecurityEnable('content')
const allowCheckImage = checkContentSecurityEnable('image')
// 并行检测
const parallel = []
// 检测标题
if (allowCheckContent && updateData.title) {
parallel.push(checkContentSec(updateData.title, clientInfo.requestId, '标题存在敏感字,请修改后提交'))
}
// 检测摘要
if (allowCheckContent && updateData.excerpt) {
parallel.push(checkContentSec(updateData.excerpt, clientInfo.requestId, '摘要存在敏感字,请修改后提交'))
}
// 检测内容
if (allowCheckContent && updateData.content) {
parallel.push(checkContentSec(JSON.stringify(updateData.content), clientInfo.requestId, '内容存在敏感字,请修改后提交'))
}
// 检测封面图
if (allowCheckImage && updateData.thumbnail) {
parallel.push(checkImageSec(updateData.thumbnail, clientInfo.requestId, '封面图存在违规,请修改后提交'))
}
// 等待所有并行检测完成
await Promise.all(parallel)
},
// 读取文章后触发
afterRead: async function ({userInfo, clientInfo, result, where, field}) {
const isAdmin = field && field.length && field.includes('is_admin')
// 检查是否配置了clientAppIds字段如果没有则抛出错误
if ((!config.clientAppIds || !config.clientAppIds.length) && !isAdmin) {
throw new Error('请在 uni-cms 配置文件中配置 clientAppIds 字段后访问详见https://uniapp.dcloud.net.cn/uniCloud/uni-cms.html#uni-cms-config')
}
// 如果clientAppIds字段未配置或当前appId不在clientAppIds中则返回
if (!config.clientAppIds || !config.clientAppIds.includes(clientInfo.appId)) return
// 获取广告配置
const adConfig = config.adConfig || {}
// 获取文章id
const id = where && where._id
// 如果id不存在或者field不包含content则返回
if (id && field.includes('content')) {
// 读取了content字段后view_count加1
await db.collection(articleDBName).where(where).update({
view_count: db.command.inc(1)
})
}
// 如果查询结果为空,则返回
if (!result.data || result.data.length <= 0) return
// 获取文章
const article = result.data[0]
// 如果文章内容不存在,则返回
if (!article.content) return
let needUnlock = false
let unlockContent = []
// 获取文章内容中的图片
article.content_images = article.content.ops.reduce((imageBlocks, block) => {
if (!block.insert.image) return imageBlocks
const {attributes} = block
const {'data-custom': custom = ""} = attributes || {}
const parseCustom = custom.split('&').reduce((obj, item) => {
const [key, value] = item.split('=')
obj[key] = value
return obj
})
return imageBlocks.concat(
parseCustom.source ||
block.insert.image
)
}, [])
for (const op of article.content.ops) {
unlockContent.push(op)
// 遍历文章内容,找到解锁内容
if (op.insert.unlockContent) {
needUnlock = true
break
}
}
// 如果文章不需要解锁,则返回
if (!needUnlock) {
article.content = getRenderableArticleContent(article.content, clientInfo)
return
}
// 获取唯一标识符
const uniqueId = adConfig.watchAdUniqueType === 'user' ? userInfo.uid : clientInfo.deviceId
// 如果未登录或者文章未解锁,则返回解锁内容
if (!uniqueId || !article._id) {
article.content = getRenderableArticleContent({
ops: unlockContent
}, clientInfo)
return
}
// 查询解锁记录
const unlockRecord = await db.collection(unlockContentDBName).where({
unique_id: uniqueId,
article_id: article._id
}).get()
// 如果未解锁,则返回解锁内容
if (unlockRecord.data && unlockRecord.data.length <= 0) {
article.content = getRenderableArticleContent({
ops: unlockContent
}, clientInfo)
return
}
// 将文章解锁替换为行结束符 \n
article.content = getRenderableArticleContent({
ops: article.content.ops.map(op => {
if (op.insert.unlockContent) {
op.insert = "\n"
}
return op
})
}, clientInfo)
}
}
}
function getRenderableArticleContent (rawArticleContent, clientInfo) {
const isUniAppX = /uni-app-x/i.test(clientInfo.userAgent)
if (!isUniAppX) {
const quillDeltaConverter = new QuillDeltaToJSONConverter(rawArticleContent.ops)
return quillDeltaConverter.convert()
}
const deltaOps = []
for (let i = 0; i < rawArticleContent.ops.length; i++) {
const op = rawArticleContent.ops[i]
if (typeof op.insert === 'object') {
const insertType = Object.keys(op.insert)
const blockRenderList = ['image', 'divider', 'unlockContent', 'mediaVideo']
if (insertType && insertType.length > 0 && blockRenderList.includes(insertType[0])) {
deltaOps.push({
type: insertType[0],
ops: [op]
})
// 一般块级节点后面都跟一个换行,需要把这个换行给去掉
const nextOps = rawArticleContent.ops[i + 1]
if (nextOps && nextOps.insert === '\n') {
i ++
}
continue
}
}
const currentIndex = deltaOps.length > 0 ? deltaOps.length - 1: 0
if (
typeof deltaOps[currentIndex] !== "object" ||
(deltaOps[currentIndex] && deltaOps[currentIndex].type !== 'rich-text')
) {
deltaOps.push({
type: 'rich-text',
ops: []
})
}
deltaOps[deltaOps.length - 1].ops.push(op)
}
return deltaOps.reduce((content, item) => {
const isRichText = item.type === 'rich-text'
let block = {
type: item.type,
data: isRichText ? item.ops: item.ops[0]
}
if (item.type === 'rich-text') {
const lastOp = item.ops.length > 0 ? item.ops[item.ops.length - 1]: null
if (lastOp !== null && lastOp.insert === "\n") {
item.ops.pop()
}
const quillDeltaConverter = new QuillDeltaToHtmlConverter(item.ops)
block.data = quillDeltaConverter.convert()
}
return content.concat(block)
}, [])
}

View File

@ -1,172 +0,0 @@
{
"bsonType": "object",
"required": ["user_id", "title", "content"],
"permission": {
"read": true,
"create": "'admin' in auth.role || 'CREATE_UNI_CMS_ARTICLE' in auth.permission",
"update": "'admin' in auth.role || 'UPDATE_UNI_CMS_ARTICLE' in auth.permission",
"delete": "'admin' in auth.role || 'DELETE_UNI_CMS_ARTICLE' in auth.permission"
},
"properties": {
"_id": {
"description": "存储文档 ID用户 ID系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "文章作者ID 参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id",
"defaultValue": {
"$env": "uid"
}
},
"category_id": {
"bsonType": "string",
"title": "分类",
"description": "分类 id参考`uni-news-categories`表",
"foreignKey": "uni-cms-categories._id",
"enum": {
"collection": "uni-cms-categories",
"field": "name as text, _id as value"
}
},
"title": {
"bsonType": "string",
"title": "标题",
"description": "标题",
"label": "标题",
"trim": "both"
},
"content": {
"bsonType": "object",
"title": "文章内容",
"description": "文章内容; 格式为Quill编辑器的Delta格式",
"label": "文章内容"
},
"excerpt": {
"bsonType": "string",
"title": "文章摘录",
"description": "文章摘录",
"label": "摘要",
"trim": "both"
},
"article_status": {
"bsonType": "int",
"title": "文章状态",
"description": "文章状态0 草稿箱 1 已发布",
"defaultValue": 0,
"enum": [{
"value": 0,
"text": "草稿箱"
},
{
"value": 1,
"text": "已发布"
}
]
},
"view_count": {
"bsonType": "int",
"title": "阅读数量",
"description": "阅读数量",
"defaultValue": 0,
"permission": {
"write": false
}
},
"like_count": {
"bsonType": "int",
"description": "喜欢数、点赞数",
"permission": {
"write": false
}
},
"is_sticky": {
"bsonType": "bool",
"title": "是否置顶",
"description": "是否置顶",
"permission": {
"write": false
}
},
"is_essence": {
"bsonType": "bool",
"title": "阅读加精",
"description": "阅读加精",
"permission": {
"write": false
}
},
"comment_status": {
"bsonType": "int",
"title": "开放评论",
"description": "评论状态0 关闭 1 开放",
"enum": [{
"value": 0,
"text": "关闭"
},
{
"value": 1,
"text": "开放"
}
]
},
"comment_count": {
"bsonType": "int",
"description": "评论数量",
"permission": {
"write": false
}
},
"last_comment_user_id": {
"bsonType": "string",
"description": "最后回复用户 id参考`uni-id-users` 表",
"foreignKey": "uni-id-users._id"
},
"thumbnail": {
"bsonType": "array",
"title": "封面大图",
"description": "缩略图地址",
"label": "封面大图",
"defaultValue": []
},
"publish_date": {
"bsonType": "timestamp",
"title": "发表时间",
"description": "发表时间",
"defaultValue": {
"$env": "now"
}
},
"publish_ip": {
"bsonType": "string",
"title": "发布文章时IP地址",
"description": "发表时 IP 地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"last_modify_date": {
"bsonType": "timestamp",
"title": "最后修改时间",
"description": "最后修改时间",
"defaultValue": {
"$env": "now"
}
},
"last_modify_ip": {
"bsonType": "string",
"description": "最后修改时 IP 地址",
"forceDefaultValue": {
"$env": "clientIP"
}
},
"preview_secret": {
"bsonType": "string",
"description": "文章预览密钥"
},
"preview_expired": {
"bsonType": "timestamp",
"description": "文章预览过期时间"
}
}
}

View File

@ -1,49 +0,0 @@
{
"bsonType": "object",
"required": [
"user_id",
"trans_id",
"content_id"
],
"permission": {
"read": true,
"create": true,
"update": false,
"delete": false
},
"properties": {
"_id": {
"description": "存储文档 ID用户 ID系统自动生成"
},
"unique_id": {
"bsonType": "string",
"description": "用于标识观看广告的唯一标识"
},
"unique_type": {
"bsonType": "string",
"description": "观看广告的唯一标识类型user 用户device 设备"
},
"trans_id": {
"bsonType": "string",
"title": "交易ID",
"description": "广告回调传回的交易ID",
"label": "内容id",
"trim": "both"
},
"content_id": {
"bsonType": "string",
"title": "内容id",
"description": "内容(文章)ID",
"label": "内容id",
"trim": "both"
},
"create_date": {
"bsonType": "timestamp",
"title": "创建时间",
"description": "创建时间",
"defaultValue": {
"$env": "now"
}
}
}
}

View File

@ -1,6 +0,0 @@
## 0.0.32022-11-11
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.22021-04-16
- 修改插件package信息
## 0.0.12021-03-15
- 初始化项目

View File

@ -1,81 +0,0 @@
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}

View File

@ -1,93 +0,0 @@
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema插件使用者在HBuilderX内编写config.json文件时会有更好的提示后续HBuilderX会提供支持
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖请参考[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置注意uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置返回7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置返回300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置如果不存在则取传入的默认值返回600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件require
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件true: 文件存在false: 文件不存在
```

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0"
}

View File

@ -1,12 +0,0 @@
{
"schedule": {
"__UNI__xxxxxx": {
"enable": true,
"h5-weixin": {
"enable": false,
"tasks": ["ticket"]
}
}
},
"ipWhitelist": ["0.0.0.0"]
}

View File

@ -1,10 +0,0 @@
## 1.1.12024-03-26
支持支付宝小程序云
## 1.1.02022-07-13
新增,应用[pages_init](https://uniapp.dcloud.io/plugin/publish.html#pages-init)当导入插件到项目工程时自动合并本插件的页面路由到项目的pages.json
## 1.0.52022-07-13
新增,应用[pages_init](https://uniapp.dcloud.io/plugin/publish.html#pages-init)当导入插件到项目工程时自动合并本插件的页面路由到项目的pages.json
## 1.0.42021-09-26
为了数据安全,`opendb-feedback`表的`permission``delete``update`的值默认为`false`
## 1.0.32021-08-26
删除多余的云函数test2

View File

@ -1,98 +0,0 @@
// 表单校验规则由 schema2code 生成,不建议直接修改校验规则,而建议通过 schema2code 生成, 详情: https://uniapp.dcloud.net.cn/uniCloud/schema
const validator = {
"content": {
"rules": [
{
"required": true
},
{
"format": "string"
}
],
"label": "留言内容/回复内容"
},
"imgs": {
"rules": [
{
"format": "array"
},
{
"arrayType": "file"
},
{
"maxLength": 6
}
],
"label": "图片列表"
},
"contact": {
"rules": [
{
"format": "string"
}
],
"label": "联系人"
},
"mobile": {
"rules": [
{
"format": "string"
},
{
"pattern": "^\\+?[0-9-]{3,20}$"
}
],
"label": "联系电话"
}
}
const enumConverter = {}
function filterToWhere(filter, command) {
let where = {}
for (let field in filter) {
let { type, value } = filter[field]
switch (type) {
case "search":
if (typeof value === 'string' && value.length) {
where[field] = new RegExp(value)
}
break;
case "select":
if (value.length) {
let selectValue = []
for (let s of value) {
selectValue.push(command.eq(s))
}
where[field] = command.or(selectValue)
}
break;
case "range":
if (value.length) {
let gt = value[0]
let lt = value[1]
where[field] = command.and([command.gte(gt), command.lte(lt)])
}
break;
case "date":
if (value.length) {
let [s, e] = value
let startDate = new Date(s)
let endDate = new Date(e)
where[field] = command.and([command.gte(startDate), command.lte(endDate)])
}
break;
case "timestamp":
if (value.length) {
let [startDate, endDate] = value
where[field] = command.and([command.gte(startDate), command.lte(endDate)])
}
break;
}
}
return where
}
export { validator, enumConverter, filterToWhere }

View File

@ -1,90 +0,0 @@
{
"id": "uni-feedback",
"displayName": "问题反馈页面模板",
"version": "1.1.1",
"description": "问题反馈页面模板,方便开发者快速搭建问题反馈界面",
"keywords": [
"问题反馈页面模板"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-page"
},
"uni_modules": {
"dependencies": [
"uni-forms",
"uni-file-picker",
"uni-easyinput",
"uni-load-more",
"uni-list",
"uni-fab",
"uni-link"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "u",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "y"
}
}
}
}
}

View File

@ -1,113 +0,0 @@
<template>
<view class="container">
<unicloud-db ref="udb" v-slot:default="{data, loading, error, options}" :options="options" collection="opendb-feedback" field="content,imgs,contact,mobile" :where="queryWhere" :getone="true" :manual="true">
<view v-if="error">{{error.message}}</view>
<view v-else-if="loading">
<uni-load-more :contentText="loadMore" status="loading"></uni-load-more>
</view>
<view v-else-if="data">
<view>
<text>留言内容/回复内容</text>
<text>{{data.content}}</text>
</view>
<view>
<text>图片列表</text>
<template v-for="(file, j) in data.imgs">
<uni-file-picker v-if="file.fileType == 'image'" :value="file" :file-mediatype="file.fileType" return-type="object" readonly></uni-file-picker>
<uni-link v-else :href="file.url" :text="file.url"></uni-link>
</template>
</view>
<view>
<text>联系人</text>
<text>{{data.contact}}</text>
</view>
<view>
<text>联系电话</text>
<text>{{data.mobile}}</text>
</view>
</view>
</unicloud-db>
<view class="btns">
<button type="primary" @click="handleUpdate">修改</button>
<button type="warn" class="btn-delete" @click="handleDelete">删除</button>
</view>
</view>
</template>
<script>
// 由schema2code生成包含校验规则和enum静态数据
import { enumConverter } from '../../js_sdk/validator/opendb-feedback.js';
export default {
data() {
return {
queryWhere: '',
loadMore: {
contentdown: '',
contentrefresh: '',
contentnomore: ''
},
options: {
// 将scheme enum 属性静态数据中的value转成text
...enumConverter
}
}
},
onLoad(e) {
this._id = e.id
},
onReady() {
if (this._id) {
this.queryWhere = '_id=="' + this._id + '"'
}
},
methods: {
handleUpdate() {
// 打开修改页面
uni.navigateTo({
url: './edit?id=' + this._id,
events: {
// 监听修改页面成功修改数据后, 刷新当前页面数据
refreshData: () => {
this.$refs.udb.loadData({
clear: true
})
}
}
})
},
handleDelete() {
this.$refs.udb.remove(this._id, {
success: (res) => {
// 删除数据成功后跳转到list页面
uni.navigateTo({
url: './list'
})
}
})
}
}
}
</script>
<style>
.container {
padding: 10px;
}
.btns {
margin-top: 10px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
}
.btns button {
flex: 1;
}
.btn-delete {
margin-left: 10px;
}
</style>

View File

@ -1,167 +0,0 @@
<template>
<view class="uni-container">
<uni-forms ref="form" :value="formData" validate-trigger="submit" err-show-type="toast">
<uni-forms-item name="content" label="留言内容/回复内容" required>
<textarea @input="binddata('content', $event.detail.value)" class="uni-textarea-border" v-model="formData.content" trim="right"></textarea>
</uni-forms-item>
<uni-forms-item name="imgs" label="图片列表">
<uni-file-picker file-mediatype="image" :limit="6" return-type="array" v-model="formData.imgs"></uni-file-picker>
</uni-forms-item>
<uni-forms-item name="contact" label="联系人">
<uni-easyinput v-model="formData.contact" trim="both"></uni-easyinput>
</uni-forms-item>
<uni-forms-item name="mobile" label="联系电话">
<uni-easyinput v-model="formData.mobile" trim="both"></uni-easyinput>
</uni-forms-item>
<view class="uni-button-group">
<button type="primary" class="uni-button" @click="submit">提交</button>
</view>
</uni-forms>
</view>
</template>
<script>
import { validator } from '../../js_sdk/validator/opendb-feedback.js';
const db = uniCloud.database();
const dbCollectionName = 'opendb-feedback';
function getValidator(fields) {
let result = {}
for (let key in validator) {
if (fields.indexOf(key) > -1) {
result[key] = validator[key]
}
}
return result
}
export default {
data() {
let formData = {
"content": "",
"imgs": [],
"contact": "",
"mobile": ""
}
return {
formData,
formOptions: {},
rules: {
...getValidator(Object.keys(formData))
}
}
},
onLoad(e) {
if (e.id) {
const id = e.id
this.formDataId = id
this.getDetail(id)
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
/**
* 触发表单提交
*/
submit() {
uni.showLoading({
mask: true
})
this.$refs.form.validate().then((res) => {
this.submitForm(res)
}).catch(() => {
uni.hideLoading()
})
},
submitForm(value) {
// 使用 clientDB 提交数据
db.collection(dbCollectionName).doc(this.formDataId).update(value).then((res) => {
uni.showToast({
icon: 'none',
title: '修改成功'
})
this.getOpenerEventChannel().emit('refreshData')
setTimeout(() => uni.navigateBack(), 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
},
/**
* 获取表单数据
* @param {Object} id
*/
getDetail(id) {
uni.showLoading({
mask: true
})
db.collection(dbCollectionName).doc(id).field("content,imgs,contact,mobile").get().then((res) => {
const data = res.result.data[0]
if (data) {
this.formData = data
}
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
}
}
}
</script>
<style>
.uni-container {
padding: 15px;
}
.uni-input-border,
.uni-textarea-border {
width: 100%;
font-size: 14px;
color: #666;
border: 1px #e5e5e5 solid;
border-radius: 5px;
box-sizing: border-box;
}
.uni-input-border {
padding: 0 10px;
height: 35px;
}
.uni-textarea-border {
padding: 10px;
height: 80px;
}
.uni-button-group {
margin-top: 50px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
}
.uni-button {
width: 184px;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
line-height: 1;
margin: 0;
}
</style>

View File

@ -1,70 +0,0 @@
<template>
<view class="container">
<unicloud-db ref="udb" v-slot:default="{data, pagination, loading, hasMore, error}" collection="opendb-feedback" field="content,imgs,contact,mobile">
<view v-if="error">{{error.message}}</view>
<view v-else-if="data">
<uni-list>
<uni-list-item v-for="(item, index) in data" :key="index" showArrow :clickable="true" @click="handleItemClick(item._id)">
<template v-slot:body>
<text>
<!-- 此处默认显示为_id请根据需要自行修改为其他字段 -->
<!-- 如果使用了联表查询请参考生成的 admin 项目中 list.vue 页面的绑定字段的写法 -->
{{item._id}}
</text>
</template>
</uni-list-item>
</uni-list>
</view>
<uni-load-more :status="loading?'loading':(hasMore ? 'more' : 'noMore')"></uni-load-more>
</unicloud-db>
<uni-fab ref="fab" horizontal="right" vertical="bottom" :pop-menu="false" @fabClick="fabClick" />
</view>
</template>
<script>
export default {
data() {
return {
loadMore: {
contentdown: '',
contentrefresh: '',
contentnomore: ''
}
}
},
onPullDownRefresh() {
this.$refs.udb.loadData({
clear: true
}, () => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
this.$refs.udb.loadMore()
},
methods: {
handleItemClick(id) {
uni.navigateTo({
url: './detail?id=' + id
})
},
fabClick() {
// 打开新增页面
uni.navigateTo({
url: './opendb-feedback',
events: {
// 监听新增数据成功后, 刷新当前页面数据
refreshData: () => {
this.$refs.udb.loadData({
clear: true
})
}
}
})
}
}
}
</script>
<style>
</style>

View File

@ -1,141 +0,0 @@
<template>
<view class="uni-container">
<uni-forms ref="form" :value="formData" validate-trigger="submit" err-show-type="toast">
<uni-forms-item name="content" label="留言内容/回复内容" required>
<textarea @input="binddata('content', $event.detail.value)" class="uni-textarea-border"
v-model="formData.content" trim="right"></textarea>
</uni-forms-item>
<uni-forms-item name="imgs" label="图片列表">
<uni-file-picker file-mediatype="image" :limit="6" return-type="array" v-model="formData.imgs">
</uni-file-picker>
</uni-forms-item>
<uni-forms-item name="contact" label="联系人">
<uni-easyinput v-model="formData.contact" trim="both"></uni-easyinput>
</uni-forms-item>
<uni-forms-item name="mobile" label="联系电话">
<uni-easyinput v-model="formData.mobile" trim="both"></uni-easyinput>
</uni-forms-item>
<view class="uni-button-group">
<button type="primary" class="uni-button" @click="submit">提交</button>
</view>
</uni-forms>
</view>
</template>
<script>
import {
validator
} from '../../js_sdk/validator/opendb-feedback.js';
console.log(validator);
const db = uniCloud.database();
const dbCollectionName = 'opendb-feedback';
function getValidator(fields) {
let result = {}
for (let key in validator) {
if (fields.indexOf(key) > -1) {
result[key] = validator[key]
}
}
return result
}
export default {
data() {
let formData = {
"content": "",
"imgs": [],
"contact": "",
"mobile": ""
}
return {
formData,
formOptions: {},
rules: {
...getValidator(Object.keys(formData))
}
}
},
onReady() {
this.$refs.form.setRules(this.rules)
},
methods: {
/**
* 触发表单提交
*/
submit() {
uni.showLoading({
mask: true
})
this.$refs.form.validate().then((res) => {
this.submitForm(res)
}).catch(() => {
uni.hideLoading()
})
},
submitForm(value) {
// 使用 clientDB 提交数据
db.collection(dbCollectionName).add(value).then((res) => {
uni.showToast({
icon: 'none',
title: '提交成功'
})
this.getOpenerEventChannel().emit('refreshData')
setTimeout(() => uni.navigateBack(), 500)
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
}).finally(() => {
uni.hideLoading()
})
}
}
}
</script>
<style>
.uni-container {
padding: 15px;
}
.uni-input-border,
.uni-textarea-border {
width: 100%;
font-size: 14px;
color: #666;
border: 1px #e5e5e5 solid;
border-radius: 5px;
box-sizing: border-box;
}
.uni-input-border {
padding: 0 10px;
height: 35px;
}
.uni-textarea-border {
padding: 10px;
height: 80px;
}
.uni-button-group {
margin-top: 50px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
justify-content: center;
}
.uni-button {
width: 184px;
padding: 12px 20px;
font-size: 14px;
border-radius: 4px;
line-height: 1;
margin: 0;
}
</style>

View File

@ -1 +0,0 @@
这是一个问题反馈客户端插件admin端插件[https://ext.dcloud.net.cn/plugin?id=4992](https://ext.dcloud.net.cn/plugin?id=4992)

View File

@ -1,72 +0,0 @@
{
"bsonType": "object",
"required": ["content"],
"permission": {
"create": "auth.uid != null",
"read": true,
"delete": false,
"update": false
},
"properties": {
"_id": {
"description": "ID系统自动生成"
},
"user_id": {
"bsonType": "string",
"description": "留言反馈用户ID\/回复留言用户ID参考uni-id-users表",
"foreignKey": "uni-id-users._id",
"forceDefaultValue": {
"$env": "uid"
}
},
"create_date": {
"bsonType": "timestamp",
"title": "留言时间\/回复留言时间",
"forceDefaultValue": {
"$env": "now"
}
},
"content": {
"bsonType": "string",
"title": "留言内容\/回复内容",
"componentForEdit": {
"name": "textarea"
},
"trim": "right"
},
"imgs": {
"bsonType": "array",
"arrayType": "file",
"maxLength": 6,
"fileMediaType": "image",
"title": "图片列表"
},
"is_reply": {
"bsonType": "bool",
"title": "是否是回复类型"
},
"feedback_id": {
"bsonType": "string",
"title": "被回复留言ID"
},
"contact": {
"bsonType": "string",
"title": "联系人",
"trim": "both"
},
"mobile": {
"bsonType": "string",
"title": "联系电话",
"pattern": "^\\+?[0-9-]{3,20}$",
"trim": "both"
},
"reply_count": {
"permission": {
"write": false
},
"bsonType": "int",
"title": "被回复条数",
"defaultValue": 0
}
}
}

View File

@ -1,36 +0,0 @@
## 1.0.182024-07-08
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
## 1.0.172024-04-26
- 兼容uni-app-x对客户端uniPlatform的调整uni-app-x内uniPlatform区分app-android、app-ios
## 1.0.162023-04-25
- 新增maxTokenLength配置用于限制数据库用户记录token数组的最大长度
## 1.0.152023-04-06
- 修复部分语言国际化出错的Bug
## 1.0.142023-03-07
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.132022-07-21
- 修复 创建token时未传角色权限信息生成的token不正确的bug
## 1.0.122022-07-15
- 提升与旧版本uni-id的兼容性补充读取配置文件时回退平台app-plus、h5但是仍推荐使用新平台名进行配置app、web
## 1.0.112022-07-14
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
## 1.0.102022-07-11
- 将token存储在用户表的token字段内与旧版本uni-id保持一致
## 1.0.92022-07-01
- checkToken兼容token内未缓存角色权限的情况此时将查库获取角色权限
## 1.0.82022-07-01
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
## 1.0.72022-06-30
- 修复config文件不合法时未抛出具体错误的Bug
## 1.0.62022-06-28
- 移除插件内的数据表schema
## 1.0.52022-06-27
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
## 1.0.42022-06-27
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
## 1.0.22022-06-23
- 对齐旧版本uni-id默认配置
## 1.0.12022-06-22
- 补充对uni-config-center的依赖
## 1.0.02022-06-21
- 提供uni-id token创建、校验、刷新接口简化旧版uni-id公共模块

View File

@ -1,85 +0,0 @@
{
"id": "uni-id-common",
"displayName": "uni-id-common",
"version": "1.0.18",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
"uniCloud",
"token",
"权限"
],
"repository": "https://gitcode.net/dcloud/uni-id-common",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"uni_modules": {
"dependencies": ["uni-config-center"],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y",
"alipay": "n"
},
"client": {
"Vue": {
"vue2": "u",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@ -1,3 +0,0 @@
# uni-id-common
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)

File diff suppressed because one or more lines are too long

View File

@ -1,16 +0,0 @@
{
"name": "uni-id-common",
"version": "1.0.18",
"description": "uni-id token生成、校验、刷新",
"main": "index.js",
"homepage": "https://uniapp.dcloud.io/uniCloud/uni-id-common.html",
"repository": {
"type": "git",
"url": "git+https://gitee.com/dcloud/uni-id-common.git"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:../../../../../uni-config-center/uniCloud/cloudfunctions/common/uni-config-center"
}
}

View File

@ -1,181 +0,0 @@
## 1.1.202024-04-28
- uni-id-co 兼容uni-app-x对客户端uniPlatform的调整uni-app-x内uniPlatform区分app-android、app-ios
## 1.1.192024-03-20
- uni-id-co 修复 实人认证的认证照片在阿里云服务空间没有保存到指定路径下的Bug
- uni-id-co 修复 云对象开发依赖未移除的Bug
## 1.1.182024-02-20
- 修复 PC设置头像无效的问题
## 1.1.172023-12-14
- uni-id-co 移除一键登录、短信的调用凭据
## 1.1.162023-10-18
- 修复 当不满足一键登录时页面回退无法继续登录的问题
## 1.1.152023-07-13
- uni-id-co 修复 QQ登录时不存在头像时报错的问题
## 1.1.142023-05-19
- 修复 退出登录不会跳转至登录页的问题
## 1.1.132023-05-10
- 修复 启用摇树优化 报错的问题
## 1.1.122023-05-05
- uni-id-co 新增 调用 add-user 接口创建用户时允许触发 beforeRegister 钩子方法beforeRegister 钩子[详见](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register)
- uni-id-co 新增 自无 unionid 到有 unionid 状态进行登录时为用户补充 unionid 字段
- uni-id-co 修复 i18n 在特定场景下报错的 bug
- uni-id-co 修复 跨平台解绑微信/QQ时无法解绑的 bug
- uni-id-co 修复 微信小程序等平台创建验证码时无法展示的 bug
- uni-id-co 修复 更新 push_clientid 时因 device_id 没有变化导致无法更新
## 1.1.112023-03-24
- 修复 tabbar页面因为token无效而强制跳转至登录页面url参数包含`uniIdRedirectUrl`)后无法返回的问题
## 1.1.102023-03-24
- 修复 PC微信扫码登录跳转地址错误
- uni-id-co 新增 请求鉴权支持 uni-cloud-s2s 模块验证签名 [uni-cloud-s2s文档](https://uniapp.dcloud.net.cn/uniCloud/uni-cloud-s2s.html)
## 1.1.92023-03-24
- 修复 跳转至登录页面的url参数包含`uniIdRedirectUrl`后无法返回的问题
## 1.1.82023-03-02
- 修复 调试模式下没有对微信授权手机号登录方式进行配置检测
## 1.1.72023-02-27
- 【重要】新增 实名认证功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#frv)
## 1.1.62023-02-24
- uni-id-co 新增 注册用户时允许配置默认角色 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#config-defult-role)
- uni-id-co 优化 `updateUserInfoByExternal`接口,允许修改头像、性别
- uni-id-co 修复 请求签名密钥字段 `requestAuthSecret` 缺少为空判断
- uni-id-co 修复 `externalRegister`接口头像未使用`avatar_file`字段保存
- 修复 web微信登录回调地址不正确
## 1.1.52023-02-23
- 更新 微信小程序端 更新头像信息,如果是使用微信的头像则不再调用裁剪接口
## 1.1.42023-02-21
- 修复 部分情况下 `uniIdRedirectUrl` 参数无效的问题
## 1.1.32023-02-20
- 修复 非微信小程序端报`TypeError: uni.hideHomeButton is not a function`的问题
## 1.1.22023-02-10
- 新增 微信小程序端 首页需强制登录时,隐藏返回首页按钮
- uni-id-co 新增 外部联登后修改用户信息接口(updateUserInfoByExternal) [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo)
- uni-id-co 优化外部联登接口(登录、注册)逻辑
## 1.1.12023-02-02
- 新增 微信小程序端 支持选择使用微信资料的“头像”和“昵称” 设置用户资料 [详情参考](https://wdoc-76491.picgzc.qpic.cn/MTY4ODg1MDUyNzQyMDUxNw_21263_rTNhg68FTngQGdvQ_1647431233?w=1280&h=695.7176470588236)
## 1.1.02023-01-31
- 【重要】优化 小程序端资源包大小运行时大小为731KB发行后为583KB可以直接将本插件作为分包使用
- 更新 微信小程序端 上传头像功能 用`wx.cropImage`实现图片裁剪
- 修复 选择一键登录时会先显示 非密码登录页面的问题
- 修复 一键登录 点击右上角的关闭按钮没有返回上一页的问题
## 1.0.412023-01-16
- 优化 压缩依赖的文件资源大小
## 1.0.402023-01-16
- 更新依赖的 验证码插件`uni-captcha`版本的版本为 0.6.4 修复 部分情况下APP端无法获取验证码的问题 [详情参考](https://ext.dcloud.net.cn/plugin?id=4048)
- 修复 客户端token过期后点击退出登录按钮报错的问题
- uni-id-co 修复 updateUser 接口`手机号``邮箱`参数值为空字符串时,修改无效的问题
## 1.0.392022-12-28
- uni-id-co 修复 URL化时第三方登录无法获取 uniPlatform 参数
- uni-id-co 修复 validator error
## 1.0.382022-12-26
- uni-id-co 优化 手机号与邮箱验证规则为空字符串时不校验
## 1.0.372022-12-09
- 优化admin端样式
## 1.0.362022-12-08
- uni-id-co 修复 `updateUser` 接口部分参数为空时数据修改异常
## 1.0.352022-11-30
- uni-id-co 新增 匹配到的用户不可在当前应用登录时的错误码 `uni-id-account-not-exists-in-current-app` [错误码说明](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#errcode)
## 1.0.342022-11-29
- 优化 toast 错误提示时间为3秒
- uni-id-co 修复 无法从 clientInfo 中获取 uniIdToken
## 1.0.332022-11-25
- uni-id-co 新增 外部系统联登接口可为外部系统创建与uni-id相对应的账号使该账号可以使用依赖uniId的系统及功能 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external)
- uni-id-co 新增 URL化请求时鉴权签名验证 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#http-reqeust-auth)
- uni-id-co 修复 微信登录时用户未设置头像的报错问题
## 1.0.322022-11-21
- 新增 设置密码页面
- 新增 登录后跳转设置密码页面配置项`setPasswordAfterLogin` [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd-after-login)
- uni-id-co 新增 设置密码接口 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd)
## 1.0.312022-11-16
- uni-id-co 修复 验证码可能无法收到的bug
## 1.0.302022-11-11
- uni-id-co 修复 用户只有openid时绑定微信/QQ报错
## 1.0.292022-11-10
- uni-id-co 支持URL化方式请求 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#adapter-http)
## 1.0.282022-11-09
- uni-id-co 升级密码加密算法支持hmac-sha256加密 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-safe)
- uni-id-co 新增 开发者可以自定义密码加密规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#custom-password-encrypt)
- uni-id-co 新增 支持将其他系统用户迁移至uni-id [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#move-users-to-uni-id)
## 1.0.272022-10-26
- uni-id-co 新增 secureNetworkHandshakeByWeixin 接口,用于建立和微信小程序的安全网络连接
## 1.0.262022-10-18
- 修复 uni-id-pages 导入时异常的Bug
## 1.0.252022-10-14
- uni-id-co 增加 微信授权手机号登录方式 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin-mobile)
- uni-id-co 增加 解绑第三方平台账号 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-third-account)
- uni-id-co 微信绑定手机号支持通过`getPhoneNumber`事件回调的`code`绑定 [文档](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin)
- 修复 sendSmsCode 接口未在参数内传递 templateId 时 未能从配置文件读取 templateId 的Bug
## 1.0.242022-10-08
- 修复 报uni-id-users表schema内错误的bug
## 1.0.232022-10-08
- 修复 vue3下vite编译发行打包失败
- 修复 某些情况下注册账号报TypeErroeCannot read properties of undefined reading showToast的错误
## 1.0.222022-09-23
- 修复 某些情况下修改密码报“两次输入密码不一致”的bug
## 1.0.212022-09-21
- 修复 store.hasLogin的值在某些情况下会出错的bug
## 1.0.202022-09-21
- 新增 store 账号信息状态管理,详情:用户中心页面 路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`
## 1.0.192022-09-20
- 修复 小程序端,使用将自定义节点设置成[虚拟节点](https://uniapp.dcloud.net.cn/tutorial/vue-api.html#%E5%85%B6%E4%BB%96%E9%85%8D%E7%BD%AE)的uni-ui组件样式错乱的问题
## 1.0.182022-09-20
- 修复 微信小程序端 WXSS 编译报错的bug
## 1.0.172022.09-19
- 修复 无法退出登录的bug
## 1.0.162022-09-19
- 修复 在 Edge 浏览器下 input[type="password"] 会出现浏览器自带的密码查看按钮
- 优化 退出登录重定向页面为 uniIdRouter.loginPage
- 新增 注册账号页面支持返回登录页面
## 1.0.152022-09-19
- 更新表结构解决在uni-admin中部分clientDB操作没有权限的问题
## 1.0.142022-09-16
- 修改 配置项`isAdmin`默认值为`false`
## 1.0.132022-09-16
- 新增 管理员注册页面
- 新增 配置项`isAdmin`区分是否为管理端
- 新增 登录成功后自动跳转;跳转优先级:路由携带(`uniIdRedirectUrl`参数) > 返回上一路由 > 跳转首页
- uni-id-co 优化 注册管理员时管理员存在提示文案
## 1.0.122022-09-07
- 修复 getSupportedLoginType判断是否支持微信公众号、PC网页微信扫码登录方式报错的Bug
- 优化 适配pc端样式
- 新增 邮箱验证码注册
- 新增 邮箱验证码找回密码
- 新增 退出登录(全局)回调事件:`uni-id-pages-logout`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听;
- 调整 抽离退出登录方法至`/uni_modules/uni-id-pages/common/common.js`中,方便在项目其他页面中调用
- 调整 用户中心(路径:`/uni_modules/uni-id-pages/pages/userinfo/userinfo`)默认不再显示退出登录按钮。支持页面传参数`showLoginManage=true`恢复显示
## 1.0.112022-09-01
- 修复 iOS端一键登录功能卡在showLoading的问题
- 更新 合并密码强度与长度配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config)
- uni-id-co 修复 调用 removeAuthorizedApp 接口报错的Bug
- uni-id-co 新增 管理端接口 updateUser [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user)
- uni-id-co 调整 为兼容旧版本未配置密码强度时提供最简单的密码规则校验长度大于6即可
- uni-id-co 调整 注册、登录时如果携带了token则尝试对此token进行登出操作
- uni-id-co 调整 管理端接口 addUser 增加 mobile、email等参数 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user)
## 1.0.102022-08-25
- 修复 导入uni-id-pages插件时未自动导入uni-open-bridge-common的Bug
## 1.0.92022-08-23
- 修复 uni-id-co 缺失uni-open-bridge-common依赖的Bug
## 1.0.82022-08-23
- 新增 H5端支持微信登录含微信公众号内的网页授权登录 和 普通浏览器内网页生成二维码,实现手机微信扫码登录)[详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#weixinlogin)
- 新增 登录成功(全局)回调事件:`uni-id-pages-login-success`,支持通过[uni.$on](https://uniapp.dcloud.net.cn/api/window/communication.html#on)监听;
- 新增 密码强度(是否必须包含大小写字母、数字和特殊符号以及长度)配置 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#config)
- 调整 uni-id-co 密码规则调整,废除之前的简单校验,允许配置密码强度 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#password-strength)
- 调整 uni-id-co 存储用户 openid 时同时以客户端 AppId 为 Key 的副本,参考:[微信登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin)、[QQ登录](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq)
- 调整 uni-id-co 依赖 uni-open-bridge-common 存储用户 session_key、access_token 等信息 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#save-user-token)
- 新增 uni-id-co 增加 beforeRegister 钩子用户在注册前向用户记录内添加一些数据 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html#before-register)
## 1.0.72022-07-19
- 修复 uni-id-co接口 logout时没有删除token的Bug
## 1.0.62022-07-13
- 新增 允许覆盖内置校验规则 [详情](https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#custom-validator)
- 修复 app端clientInfo.appVersionCode为数字导致校验无法通过的Bug
## 1.0.52022-07-11
修复 微信小程序调用uni-id-co接口报错的Bug [详情](https://ask.dcloud.net.cn/question/148877)
## 1.0.42022-07-06
- uni-id-co增加clientInfo字段类型校验
- 监听token更新时机同步客户端push_clientid至uni-id-device表改为同步客户端push_clientid至uni-id-device表和opendb-device表
## 1.0.32022-07-05
新增监听token更新时机同步客户端push_clientid至uni-id-device表
## 1.0.22022-07-04
修复微信小程序登录时无unionid报错的Bug [详情](https://ask.dcloud.net.cn/question/148016)
## 1.0.12022-06-28
添加相关uni-id表
## 1.0.02022-06-23
正式版

Some files were not shown because too many files have changed in this diff Show More