小程序产品库完成

This commit is contained in:
aixianling
2022-02-14 17:25:54 +08:00
parent cb5f434edb
commit 8d2905428e
145 changed files with 22037 additions and 0 deletions

View File

@@ -0,0 +1,367 @@
<template>
<section class="AiAreaPicker">
<ai-search-popup mode="bottom" ref="areaSelector" length="85%">
<div slot="btn" @tap="handleInit">
<slot v-if="$slots.default"/>
<div v-else class="areaSelector">
<image :src="locationIcon" class="location"/>
<div v-text="currentArea.name"/>
</div>
</div>
<div class="areaSelector" id="areaSelector">
<div class="fixedTop">
<b>选择地区</b>
<em>选择区域</em>
<div class="selectedArea" v-if="hasSelected">
<p v-for="area in fullArea" :key="area.id" v-text="area.name"/>
<p v-if="selected.type==5" v-text="selected.name"/>
</div>
<div/>
<span v-if="all" v-text="`省`" @click="selectNode({}, -1)"/>
<span v-for="(area,i) in fullArea" :key="area.id" v-text="area.levelLabel"
@click="selectNode(area, i)"/>
</div>
</div>
<scroll-view class="fill pendingList" :style="{height: height}" scroll-y>
<div class="pendingItem flexRow" flex v-for="op in pending" :key="op.id" @tap="getChild(op)">
<div class="fill" :class="{ self: index == op.id }" v-html="op.name"/>
<u-icon v-if="index == op.id" name="checkbox-mark" color="#4181FF"/>
</div>
</scroll-view>
<div class="bottomBtns">
<div @click="closePopup">取消</div>
<div class="primary fill" @click="handleSelect">确定</div>
</div>
</ai-search-popup>
</section>
</template>
<script>
import AiSearchPopup from './AiSearchPopup'
import AiCell from './AiCell.vue'
import {mapState} from 'vuex'
export default {
name: 'AiAreaPicker',
components: {AiCell, AiSearchPopup},
props: {
areaId: {default: ''},
name: {default: ''},
value: String,
all: Boolean,
icon: {default: "location.svg"},
isHideTown: {
type: Boolean,
default: false
}
},
computed: {
...mapState(['user']),
dataRange() {
let rules = [10, 8, 6, 3, 0],
level = 0
if (this.all) return (level = 0)
rules.some((e, i) => {
let reg = new RegExp(`0{${e}}`, 'g')
if (reg.test(this.areaId || this.user.areaId || this.$areaId)) {
return (level = i)
}
})
return level
},
currentArea() {
return this.fullArea?.slice(-1)?.[0] || {}
},
locationIcon() {
return this.$cdn + this.icon
},
pending() {
return this.list.map(e => ({...e, levelLabel: this.levelLabels[e.type]}))
},
hasSelected() {
return this.fullArea?.length > 0
}
},
data() {
return {
fullArea: [],
index: '',
list: [],
height: '500px',
levelLabels: ["省", "市", "县/区", "镇/街道", "村/社区"],
selected: {}
}
},
watch: {
areaId(v) {
v && this.getFullArea()
},
fullArea: {
handler(v) {
this.$nextTick(() => {
if (v) {
this.scrollHeight()
}
})
}
}
},
methods: {
show() {
this.handleInit()
this.$refs.areaSelector.showPopup()
},
scrollHeight () {
var obj = this.createSelectorQuery()
obj.select('#areaSelector').boundingClientRect()
obj.exec(rect => {
if (rect.length) {
this.height = `calc(100% - ${rect[0].height}px)`
}
})
},
getFullArea() {
let areaId = this.areaId || (this.all ? '' : this.$areaId)
return this.$instance.post('/admin/area/getAllParentAreaId', null, {
withoutToken: true,
params: {areaId},
}).then((res) => {
if (res?.data) {
res.data.forEach((e) => {
e && (e.levelLabel = this.levelLabels[e.type])
})
if (res.data.length > 1) {
this.fullArea = res.data.reverse().slice(this.dataRange)
} else {
this.fullArea = res.data
}
return this.fullArea
}
})
},
getChildAreas(id) {
id && this.$instance.post('/admin/area/queryAreaByParentId', null, {
withoutToken: true,
params: {id},
}).then((res) => {
if (res.data.length) {
this.list = res.data
let self = this.fullArea.find((e) => e.id == this.areaId)
if (self.id && !this.isHideTown) {
this.list.unshift(self)
}
}
})
},
isVillage (areaId) {
return areaId.substr(areaId.length - 3, 3) != '000'
},
getProvinces() {
this.$instance.post('/admin/area/queryProvinceList', null, {withoutToken: true}).then((res) => {
if (res?.data) {
this.list = res.data
}
})
},
handleSelect() {
this.$emit('select', this.index)
let fullName = ''
this.fullArea.forEach(v => {
fullName = fullName + v.name
})
if (this.selected.type == 5) {
fullName = fullName + this.selected.name
}
this.$emit('update:fullName', fullName)
this.$emit('update:name', this.selected.name)
this.closePopup()
},
getChild(op) {
if (op.id != this.index) {
if (op.type < 5 && (/0{3}$/g.test(this.index) || !this.index)) {
this.fullArea.push(op)
this.getChildAreas(op.id)
}
this.selected = op
this.index = op.id
}
},
selectNode(area, i) {
this.fullArea.splice(i + 1, this.fullArea.length - i)
if (this.all && !area.id) {
this.index = ''
this.getProvinces()
} else {
this.index = area.id
this.getChildAreas(area.id)
}
},
handleInit() {
this.index = this.value || this.areaId
if (this.all && !this.areaId && !this.currentArea.id) {
this.getProvinces()
return false
}
this.getFullArea().then(() => {
this.getChildAreas(this.currentArea.id || this.areaId)
})
},
closePopup() {
this.$refs.areaSelector?.handleSelect()
}
}
}
</script>
<style lang="scss" scoped>
.AiAreaPicker {
::v-deep .areaSelector {
display: flex;
align-items: center;
span {
cursor: pointer;
color: #333;
font-weight: bold;
line-height: 112px;
margin-right: 72px;
position: relative;
&:last-of-type {
margin-right: 0;
&:after {
content: " ";
display: block;
position: absolute;
bottom: -26px;
left: 50%;
transform: translate(-50%, 100%);
width: 40px;
height: 8px;
background: #4181FF;
border-radius: 4px;
}
}
}
b {
display: block;
width: 100%;
line-height: 96px;
text-align: center;
}
em {
font-style: normal;
font-size: 24px;
font-weight: 400;
color: #999999;
line-height: 34px;
}
.selectedArea {
display: flex;
align-items: center;
width: fit-content;
max-width: calc(100vw - 128px);
overflow: hidden;
text-overflow: ellipsis;
padding: 0 32px;
height: 80px;
background: #ECF2FF;
border-radius: 40px;
font-size: 32px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #4181FF !important;
margin: 16px 0 32px;
white-space: nowrap;
}
.fixedTop {
// position: fixed;
top: 0;
width: 100%;
left: 0;
background: #fff;
border-bottom: 4px solid #f5f5f5;
z-index: 1;
text-align: start;
padding: 0 32px;
box-sizing: border-box;
}
}
::v-deep.u-drawer-content {
.areaSelector {
box-sizing: border-box;
border-bottom: 1px solid #f5f5f5;
}
}
::v-deep .pendingList {
padding: 0 32px 120px;
box-sizing: border-box;
.pendingItem {
color: #333;
height: 104px;
text-align: start;
.self {
font-weight: bold;
}
}
}
::v-deep.bottomBtns {
position: fixed;
left: 0;
bottom: 0;
width: 100vw;
display: flex;
align-items: center;
text-align: center;
height: 120px;
font-size: 34px;
font-family: PingFangSC-Medium, PingFang SC;
font-weight: 500;
color: #3671EE;
background: #fff;
padding: 0 32px;
box-sizing: border-box;
& > div {
padding: 0 92px;
line-height: 88px;
height: 88px;
border: 1px solid #A0C0FF;
border-radius: 16px;
&.primary {
color: #fff;
background: #4181FF;
border-color: #4181FF;
}
& + div {
margin-left: 32px;
}
}
}
.location {
width: 28px;
height: 80px;
margin-right: 12px;
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<section class="AiCell" :class="{bottomBorder,alignCenter,topLabel}">
<div class="label" :class="{title}">
<slot v-if="$slots.label" name="label"/>
<span v-else>{{ label }}</span>
</div>
<div class="content" :class="{topLabel}">
<slot/>
</div>
</section>
</template>
<script>
export default {
name: "AiCell",
props: {
label: {default: ""},
bottomBorder: Boolean,
topLabel: Boolean,
title: Boolean,
alignCenter: Boolean
}
}
</script>
<style lang="scss" scoped>
.AiCell {
display: flex;
min-height: 72px;
font-size: 30px;
color: #333;
padding: 14px 0;
box-sizing: border-box;
justify-content: space-between;
&.bottomBorder {
border-bottom: 1px solid #eee;
}
&.alignCenter {
align-items: center;
}
&.topLabel {
flex-direction: column;
}
.label {
min-width: 60px;
flex-shrink: 0;
width: auto;
color: #999;
&.title {
color: #333;
font-weight: bold;
font-size: 34px;
}
}
.content {
flex: 1;
min-width: 100px;
min-height: 40px;
max-width: 500px;
text-align: right;
&.topLabel {
text-align: start;
margin-top: 16px;
max-width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<section class="AiSearchPopup">
<u-popup v-model="show" :length="length" closeable :mode="mode" border-radius="32">
<slot v-if="$slots.default"/>
<div class="searchPane" v-else>
<div class="title">{{ title }}</div>
<u-search v-model="search" :placeholder="placeholder" :show-action="false" @search="getList()" :focus="show"/>
<div class="result">
<div class="option" v-for="(op,i) in list" :key="i" @tap="handleSelect(op)">{{ op[ops.label] }}</div>
</div>
</div>
</u-popup>
<div @tap="show=true">
<slot name="btn"/>
</div>
</section>
</template>
<script>
export default {
name: "AiSearchPopup",
props: {
title: {default: "搜索"},
placeholder: {default: "请搜索"},
ops: {default: () => ({label: 'label', search: 'name'})},
url: String,
mode: {default: "right"},
length: {default: "100%"}
},
data() {
return {
show: false,
search: "",
list: []
}
},
watch: {
show(v) {
!v && this.$emit('close')
}
},
methods: {
getList() {
this.url && this.$instance.post(this.url, null, {
params: {[this.ops.search]: this.search}
}).then(res => {
if (res?.data) {
this.list = res.data
}
})
},
showPopup() {
this.show = true
},
handleSelect(op) {
this.$emit('select', op)
this.show = false
}
}
}
</script>
<style lang="scss" scoped>
.AiSearchPopup {
::v-deep .searchPane {
padding: 0 16px;
display: flex;
flex-direction: column;
height: 100%;
.title {
width: 100%;
height: 100px;
text-align: center;
line-height: 100px;
}
.result {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-bottom: 30px;
}
.option {
display: flex;
align-items: center;
height: 80px;
border-bottom: 1px solid #eee;
font-size: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div class="AiCheckbox">
<div
class="AiCheckbox-item"
v-for="(item, index) in options"
@click="onChange(item.value)"
:class="[choosed.indexOf(item.value) > -1 ? 'active' : '']"
:key="index">
{{ item.label }}
</div>
</div>
</template>
<script>
export default {
name: 'AiCheckbox',
model: {
event: 'input',
prop: 'value'
},
props: {
value: Array,
placeholder: {
default: '请选择'
},
list: {
default: () => []
},
dict: String,
disabled: Boolean
},
data () {
return {
dictKey: '',
choosed: []
}
},
computed: {
options () {
return this.dictKey ? this.$dict.getDict(this.dict).map(e => ({
value: e.dictValue,
label: e.dictName
})) : this.list
}
},
mounted () {
this.$dict.load(this.dict).then(() => {
this.dictKey = this.dict
})
},
methods: {
onChange (value) {
if (this.choosed.indexOf(value) > -1) {
this.choosed.splice(this.choosed.indexOf(value), 1)
} else {
this.choosed.push(value)
}
this.$emit('input', this.choosed)
}
}
}
</script>
<style lang="scss" scoped>
.AiCheckbox {
display: flex;
align-items: center;
flex-wrap: wrap;
.AiCheckbox-item {
width: 100%;
line-height: 1.3;
padding: 20px 32px;
margin-bottom: 16px;
text-align: center;
background: #FFFFFF;
box-sizing: border-box;
border-radius: 16px;
color: #333333;
font-size: 28px;
border: 1px solid #CCCCCC;
&.active {
background: #4181FF;
color: #fff;
border-color: #4181FF;
}
}
}
</style>

View File

@@ -0,0 +1,207 @@
<template>
<div class="AiComment">
<div class="comments flex-row">
<div class="comments-box" @click="showCommentBox">{{ boxContent }}</div>
<img src="./static/comment.svg" @click="showCommentList" alt=""/>
<text>{{ commentCount || 0 }}</text>
</div>
<div class="modalWrapper" v-if="commentBoxPopup" :class="{clickClose:!modelClickClose}"
@click="commentBoxPopup=false"/>
<div class="commentBox" v-if="commentBoxPopup" :style="{bottom:marginBottom+ 'px'}">
<textarea v-model="content" placeholder="写下你的想法…" maxlength="300" :focus="focus" @focus="getMarginBottom"
@blur="marginBottom=0" :adjust-position="false" fixed/>
<view class="flex-row form-submit">
<div>{{ `字数 ${wordCount || 0} / 300` }}</div>
<button @click="submitComment" :disabled="wordCount == 0">发布</button>
</view>
</div>
</div>
</template>
<script>
export default {
name: "AiComment",
props: {
needLogin: Boolean,
customLogin: Boolean,
commentCount: Number,
modelClickClose: {type: Boolean, default: true}
},
computed: {
wordCount() {
return this.content.length || 0
},
boxContent() {
return this.content || "我也说两句..."
},
isLogin() {
return Boolean(uni.getStorageSync('token'))
},
},
data() {
return {
content: "",
marginBottom: 0,
commentBoxPopup: false,
focus: false
}
},
methods: {
showCommentBox() {
if (this.customLogin) {
this.$emit("login", flag => this.commentBoxPopup = flag)
} else if (this.needLogin) {
if (this.isLogin) {
this.commentBoxPopup = true
} else {
this.$dialog.confirm({
content: '您还未登陆',
confirmText: '去登录'
}).then(() => {
uni.switchTab({url: '/pages/mine/mine'})
}).catch(() => {
})
}
} else {
this.commentBoxPopup = true
}
},
submitComment() {
this.commentBoxPopup = false
this.$emit("submitComment", this.content)
this.content = ""
},
showCommentList() {
this.commentBoxPopup = false
this.$emit("showCommentList")
},
getMarginBottom({detail}) {
this.marginBottom = detail.height
this.focus = true
}
}
}
</script>
<style lang="scss" scoped>
.AiComment {
.comments {
width: 100%;
height: 112px;
padding: 24px 32px;
box-sizing: border-box;
position: fixed;
bottom: 0;
background: #f7f7f7;
.comments-box {
width: 580px;
height: 64px;
line-height: 64px;
background-color: #fff;
color: #666;
font-size: 26px;
padding-left: 16px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
image {
width: 52px;
height: 52px;
margin-left: 16px;
vertical-align: middle;
}
text {
color: #666666;
font-size: 28px;
line-height: 60px;
}
}
.modalWrapper {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .6);
z-index: 9;
&.clickClose {
pointer-events: none;
}
}
.commentBox {
width: 100%;
height: 288px;
background-color: #fff;
padding: 32px 32px 24px 26px;
box-sizing: border-box;
position: fixed;
bottom: 0;
z-index: 99;
textarea {
width: 100%;
height: 144px;
color: #666;
font-size: 26px;
background: #F7F7F7;
border-radius: 16px;
padding: 16px;
box-sizing: border-box;
&::placeholder {
font-size: inherit;
}
}
.form-submit {
margin-top: 24px;
height: 64px;
justify-content: space-between;
align-items: center;
color: #999;
font-size: 24px;
button {
width: 144px;
height: 64px;
background-color: #135AB8;
color: #fff;
font-size: 24px;
border-radius: 32px;
line-height: 64px;
text-align: center;
margin: unset;
&[disabled] {
color: #999;
background-color: #f7f7f7;
font-size: 28px;
border: 0;
}
&:active {
opacity: .8;
}
&:after {
border: none;
}
}
}
}
.flex-row {
display: flex;
flex-direction: row;
}
}
</style>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="26px" height="26px" viewBox="0 0 26 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 55.2 (78181) - https://sketchapp.com -->
<title>icon/tab_bar/Comment</title>
<desc>Created with Sketch.</desc>
<g id="icon/tab_bar/Comment" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M4.75,21.7864745 L9.45344419,19.4347524 C9.6964412,19.3132539 9.96438906,19.25 10.236068,19.25 L20,19.25 C20.6903559,19.25 21.25,18.6903559 21.25,18 L21.25,8 C21.25,7.30964406 20.6903559,6.75 20,6.75 L6,6.75 C5.30964406,6.75 4.75,7.30964406 4.75,8 L4.75,21.7864745 Z" id="矩形" stroke="#333333" stroke-width="1.5"></path>
<rect id="矩形" fill="#999999" x="8" y="10.5" width="10" height="1.5" rx="0.75"></rect>
<rect id="矩形备份" fill="#999999" x="8" y="14" width="10" height="1.5" rx="0.75"></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 983 B

View File

@@ -0,0 +1,149 @@
<template>
<section class="AiDetail">
<div class="content">
<div class="title">{{detail.title}}</div>
<div class="info">
<span>{{detail.createTime}}</span>
<span class="right">
<em>{{detail.viewCount}}</em>人看过
</span>
</div>
<slot v-if="$slots.content" name="content"/>
<u-parse v-else :html="detail.content"/>
</div>
<!-- <div class="files" v-if="detail.contentType==0 && detail.files && detail.files.length">-->
<!-- <img class="file-img" :src="file.url" @click.native="preview(index)" alt="" v-for="(file,index) in detail.files" :key="index">-->
<!-- </div>-->
<div class="files" v-if="detail.contentType==1 && detail.files && detail.files.length">
<video class="file-img" :src="file.url" v-for="(file,index) in detail.files" :key="index"></video>
</div>
<div v-if="comment" class="comments">
</div>
</section>
</template>
<script>
import AiImage from "../../components/AiImage/AiImage";
import AiTopFixed from "../../components/AiTopFixed/AiTopFixed";
export default {
name: "AiDetail",
components: {AiImage, AiTopFixed},
props: {
title: {default: ""},
detail: {default: () => ({})},
comment: Boolean
},
methods: {
preview(index) {
this.$previewImage(this.detail.files,index,"url");
}
},
}
</script>
<style lang="scss" scoped>
.AiDetail {
min-height: 100vh;
background: #ffffff;
.header {
.title {
font-weight: bold;
font-size: 48px;
}
.bottomBar {
justify-content: space-between;
margin-top: 16px;
color: #999;
.u-icon + .u-icon {
margin-left: 32px;
}
}
}
.content{
padding: 32px;
background: #fff;
&.content {
margin-bottom: 20px;
.title{
font-size: 38px;
font-weight: 600;
color: #333333;
line-height: 52px;
margin-bottom: 32px;
}
.info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 64px;
font-size: 30px;
font-weight: 400;
color: #999999;
.right{
display: flex;
align-items: center;
& > em{
color: #4181FF;
}
}
}
}
}
.files {
padding: 0 32px;
background: #fff;
.file-img{
width:100%;
height:486px;
margin-bottom:32px;
}
}
.imageList {
justify-content: space-around;
.AiImage {
margin-right: 8px;
margin-bottom: 8px;
}
}
.fileList {
& > div {
margin-bottom: 8px;
}
.info {
flex: 1;
min-width: 0;
display: flex;
margin-left: 16px;
word-break: break-all;
align-items: center;
& > span {
flex: 1;
min-width: 0;
}
& > i {
flex-shrink: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="emptyWrap">
<img class="emptyImg" src="https://cdn.cunwuyun.cn/dvcp/h5/Empty.png">
<div class="emptyText">{{ description }}</div>
</div>
</template>
<script>
export default {
name: "AiEmpty",
props: {
description: {
default: '暂无相关信息',
type: String
}
}
}
</script>
<style lang="scss" scoped>
.emptyWrap {
width:100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 80px;
.emptyImg {
width: 400px;
height: 240px;
margin-top: 52px;
}
.emptyText {
font-size: 29px;
font-weight: 400;
color: rgba(183, 183, 183, 1);
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,35 @@
<template>
<section class="AiFixedBtn">
<movable-area class="movableArea">
<movable-view direction="all" x="300" y="500" @tap.stop>
<slot/>
</movable-view>
</movable-area>
</section>
</template>
<script>
export default {
name: "AiFixedBtn"
}
</script>
<style lang="scss" scoped>
.AiFixedBtn {
.movableArea {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
uni-movable-view {
pointer-events: auto;
width: auto;
height: auto;
}
}
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<section class="AiIcon">
<view v-if="!singleColor" class="ai-icon" :class="icon" :style="{width:size+'px',height:size+'px'}"/>
<view v-else class="iconfont" :style="{ fontSize: size + 'rpx', color: color }">{{ icon }}</view>
</section>
</template>
<script>
export default {
name: "AiIcon",
props: {
icon: String,
singleColor: {
type: Boolean,
default: false
},
color: {
type: String
},
size: {type: String, default: "32"}
}
}
</script>
<style lang="scss" scoped>
@import "./ai-icon.css";
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
<template>
<section class="AiImage">
<div v-if="$slots.default" @tap="prev">
<slot/>
</div>
<u-image v-else :src="image" @tap="prev">
<image v-if="link" class="errorImage" slot="error" :src="$cdn+'link.png'"/>
<image v-else-if="miniapp" class="errorImage" slot="error" :src="$cdn+'miniwxmp.jpg'"/>
<image v-else class="errorImage" slot="error" :src="$cdn+'file.png'"/>
</u-image>
</section>
</template>
<script>
import {mapActions} from "vuex";
export default {
name: "AiImage",
data() {
return {
dialog: false
}
},
computed: {
image() {
return this.src?.replace(/\\/g, '/')
}
},
props: {
src: String,
preview: Boolean,
link: Boolean,
miniapp: Boolean,
file: {
default: () => {
}
}
},
methods: {
...mapActions(['previewFile', 'injectJWeixin']),
prev() {
if (this.preview) {
if (!!this.image) {
uni.previewImage({
current: this.image,
urls: [this.image],
success() {
sessionStorage.setItem("previewImage", " 1")
}
})
} else {
this.previewFile({size: 1, ...this.file})
}
}
},
}
}
</script>
<style lang="scss" scoped>
.AiImage {
::v-deep image {
width: 160px;
height: 160px;
object-fit: cover;
}
::v-deep .u-image__error {
position: relative;
}
.errorImage {
width: 80px;
height: 80px;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="ai-login">
<u-modal v-model="userPhoneShow" showCancelButton :showTitle="true" :title="'手机号码授权'"
:content="'本次操作需要获取您的手机号码,请同意授权'" @cancel="hide">
<template slot="confirm-button">
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" class="confirm-button">同意</button>
</template>
</u-modal>
</div>
</template>
<script>
export default {
name: 'AiLogin',
data() {
return {
userPhoneShow: false,
userData: {},
code: ''
}
},
methods: {
show() {
this.$getUserProfile().then(data => {
this.userData = data.userInfo;
this.userPhoneShow = true;
})
this.$getLoginCode().then(res => {
this.code = res.code;
})
},
hide() {
this.userPhoneShow = false
},
getPhoneNumber(arg) {
const {encryptedData, errMsg, iv} = arg.detail
if (errMsg == 'getPhoneNumber:ok') {
this.$getLoginCode().then(() => {
// let body = {...this.userData, encryptedData, iv, code: res.code}
this.$instance.post(`/app/appwechatuser/getWechatUserPhone`, {
encryptedData, iv,
code: this.code
}, {withoutToken: true}).then(d => {
if (d.data) {
let data = {...this.userData, phone: d.data};
this.$autoLogin(data).then(res=>{
this.$emit("success",res);
});
}
})
})
}
}
}
}
</script>
<style lang="scss" scoped>
.confirm-button{
background-color: #fff!important;
}
.confirm-button::after{
border: none!important;
}
</style>

View File

@@ -0,0 +1,202 @@
<template>
<section class="AiNewsList">
<slot name="header"/>
<slot v-if="$slots.content" name="content"/>
<div class="list-wrap" v-if="list && list.length">
<div class="list-card" v-for="(category,index) in list" :key="index" @click="$linkTo('/subPages/contentManager/contentDetail?id='+category.id)">
<div class="header">{{category.title}}</div>
<div class="content-wrap" v-if="category.contentType==0 && category.files && category.files.length == 1">
<img class="img" :src="item.url" v-for="(item,index) in category.files" :key="index.id" alt="">
</div>
<div class="content-wrap" v-if="category.contentType==0 && category.files && category.files.length > 1">
<img class="min-img" :src="item.url" v-for="(item,index) in category.files && category.files.slice(0,3)" :key="index.id" alt="">
</div>
<div class="content-wrap" v-if="category.contentType==1">
<img class="img" :src="category.pictureUrl" alt="">
<img class="play-icon" src="../../static/img/play.png" alt="">
</div>
<div class="bottom">
<div class="left">
<div class="tag">{{category.categoryName}}</div>
{{category.createTime}}
</div>
<div class="right">
<em>{{category.viewCount}}</em>
人看过
</div>
</div>
</div>
</div>
<AiEmpty v-else></AiEmpty>
</section>
</template>
<script>
import AiTopFixed from "../AiTopFixed/AiTopFixed";
import AiEmpty from "../../components/AiEmpty/AiEmpty"
export default {
name: "AiNewsList",
components: {AiTopFixed,AiEmpty},
props: {
list: {default: () => []},
props: {
default: () => ({
title: 'title',
type: "type",
createTime: "createTime",
count: "count",
})
},
loadmore: {default: "loadmore"}
}
}
</script>
<style lang="scss" scoped>
.AiNewsList {
height: 100vh;
background: #f3f6f9;
.listPane {
padding: 24px 32px 36px;
.card {
background: #FFFFFF;
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.02);
border-radius: 16px;
padding: 32px;
margin-bottom: 32px;
.title {
height: 50px;
font-size: 36px;
font-weight: 500;
color: #333333;
line-height: 50px;
margin-bottom: 24px;
}
.subTitle {
color: #999;
width: 100%;
.tag {
height: 48px;
background: #EEEEEE;
border-radius: 24px;
text-align: center;
line-height: 48px;
margin-right: 16px;
}
.count {
color: #4181FF;
}
.right{
margin-left: auto;
}
}
}
}
.list-wrap {
box-sizing: border-box;
padding: 32px;
.list-card {
width: 100%;
min-height: 100px;
background: #FFFFFF;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.02);
border-radius: 16px;
box-sizing: border-box;
padding: 32px;
margin-bottom: 24px;
.header {
font-size: 36px;
font-weight: 600;
color: #333333;
line-height: 50px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.content-wrap {
display: flex;
gap: 4px;
flex-wrap: wrap;
margin-top: 24px;
position: relative;
.img {
width: 100%;
height: 350px;
}
.min-img{
width: 204px;
height: 204px;
}
.play-icon{
width: 80px;
height: 80px;
border-radius:50%;
position:absolute;
left:50%;
top: 50%;
transform:translate(-50%,-50%);
}
}
.bottom {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 24px;
.left {
display: flex;
align-items: center;
font-size: 28px;
font-weight: 400;
color: #999999;
.tag {
width: 144px;
height: 48px;
background: #EEEEEE;
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 400;
color: #999999;
margin-right: 16px;
}
}
.right {
font-size: 28px;
font-weight: 400;
color: #999999;
display: flex;
align-items: center;
em {
font-style: normal;
color: #4181FF;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="AiRadio" :class="[isLine ? 'AiRadio-line' : '']">
<div
class="AiRadio-item"
v-for="(item, index) in options"
@click="onChange(index)"
:class="[currIndex === index ? 'active' : '']"
:key="index">
{{ item.label }}
</div>
</div>
</template>
<script>
export default {
name: 'AiRadio',
model: {
event: 'input',
prop: 'value'
},
props: {
value: String,
placeholder: {
default: '请选择'
},
list: {
default: () => []
},
isLine: Boolean,
dict: String,
disabled: Boolean
},
data () {
return {
currIndex: -1,
dictKey: ''
}
},
computed: {
options () {
return this.dictKey ? this.$dict.getDict(this.dict).map(e => ({
value: e.dictValue,
label: e.dictName
})) : this.list
}
},
mounted () {
this.$dict.load(this.dict).then(() => {
this.dictKey = this.dict
if (this.value) {
console.log(this.value)
this.$dict.getDict(this.dict).forEach((e, i) => {
console.log(e)
if (e.dictValue === this.value) {
this.currIndex = i
}
})
}
})
},
methods: {
onChange (index) {
this.currIndex = index
const choosed = this.options[index]
this.$emit('name', choosed.label)
this.$emit('input', choosed.value)
}
}
}
</script>
<style lang="scss" scoped>
.AiRadio {
display: flex;
align-items: center;
flex-wrap: wrap;
.AiRadio-item {
width: 212px;
line-height: 1.3;
padding: 20px 32px;
margin-bottom: 16px;
margin-right: 16px;
padding: 20px 32px;
text-align: center;
background: #FFFFFF;
border-radius: 16px;
color: #333333;
font-size: 28px;
border: 1px solid #CCCCCC;
box-sizing: border-box;
&:nth-of-type(3n) {
margin-right: 0;
}
&.active {
background: #4181FF;
color: #fff;
border-color: #4181FF;
}
}
&.AiRadio-line {
width: 100%;
.AiRadio-item {
width: 100%;
margin-right: 0;
}
}
}
</style>

View File

@@ -0,0 +1,99 @@
<template>
<section class="AiSelect">
<div class="display" v-if="$slots.default" @tap="handleShowOptions">
<slot/>
</div>
<div v-else class="display" @tap="handleShowOptions">
<div class="selectedLabel" v-if="selectedLabel">{{ selectedLabel }}</div>
<i v-else>{{ placeholder }}</i>
<u-icon name="arrow-right" color="#ddd"/>
</div>
<u-select v-model="show" :list="options" :mode="mode" @confirm="handleConfirm"/>
</section>
</template>
<script>
export default {
name: "AiSelect",
model: {
event: "input",
prop: "value"
},
props: {
value: String,
placeholder: {default: "请选择"},
list: {default: () => []},
mode: {default: "single-column"},
dict: {default: ""},
disabled: Boolean
},
computed: {
selectedLabel() {
let label = this.options.find(e => e.value == this.value)?.label
return this.selected?.map(e => e.label)?.join(",") || label
},
options() {
return this.dictKey ? this.$dict.getDict(this.dict).map(e => ({
value: e.dictValue,
label: e.dictName
})) : this.list
}
},
data() {
return {
show: false,
dictKey: '',
selected: []
}
},
mounted () {
this.dict && this.$dict.load(this.dict).then(() => {
this.dictKey = this.dict
})
},
methods: {
handleConfirm(v) {
this.selected = v
this.$emit("data", this.selected)
console.log(v)
this.$emit("input", v[0].value)
this.$forceUpdate()
},
handleShowOptions() {
if (!this.disabled) this.show = true
}
}
}
</script>
<style lang="scss" scoped>
.AiSelect {
max-width: 100%;
::v-deep .u-icon {
margin-left: 8px;
}
.display {
display: flex;
align-items: center;
.selectedLabel {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
font-size: 30px;
color: #333;
white-space: nowrap;
}
}
i {
font-style: normal;
font-size: 30px;
color: $uni-text-color-grey;
}
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<section class="AiTopFixed" :style="{background}">
<!--占位区-->
<div class="placeholder">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
<!--悬浮区-->
<div class="fixed" :style="{background}">
<div v-if="$slots.tabs">
<slot name="tabs"/>
</div>
<div class="content" v-if="$slots.default">
<slot/>
</div>
</div>
</section>
</template>
<script>
export default {
name: "AiTopFixed",
props: {
background: {default: "#fff"}
}
}
</script>
<style lang="scss" scoped>
.AiTopFixed {
width: 100%;
& > div {
width: 100%;
}
.fixed {
width: 100%;
min-height: 100px;
top: 0;
position: fixed;
z-index: 9;
}
.placeholder {
visibility: hidden;
opacity: 0;
}
.content {
min-height: 100px;
padding: 20px 32px;
box-sizing: border-box;
}
::v-deep .u-search {
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.02);
margin-bottom: 32px !important;
}
}
</style>

View File

@@ -0,0 +1,146 @@
<template>
<div class="AiTransSpeech" v-if="!noSpeech">
<button title="语音播报" @click="getSpeech">
<div>
<div class="iconfont" :class="{playing:loading}">&#xe6b1;</div>
</div>
<div>{{ text }}</div>
</button>
</div>
</template>
<script>
export default {
name: "AiTransSpeech",
props: {
src: String,
content: String
},
data() {
return {
audioContext: null,
speech: "",
loading: false,
text: "开始"
}
},
computed: {
speechString() {
return this.content ? this.content.replace(/<\/?.+?\/?>/g, "") : ""
},
noSpeech() {
return !this.src && !this.content
}
},
methods: {
getSpeech() {
if (!this.noSpeech) {
if (this.audioContext) {
if (this.loading) {
this.loading = false
this.audioContext.pause()
} else {
this.loading = true
this.audioContext.play()
}
} else if (this.content) {
this.loading = true
if (!this.speech) {
this.$instance.post("/app/msc/transToSpeech" + `?fileName=demo&words=${this.speechString}`).then(res => {
if (res && res.data) {
let url = res.data.join("")
this.speech = url.substring(0, url.indexOf(";"))
this.playAudio()
}
}).catch(() => this.loading = false)
} else {
this.playAudio()
}
} else if (this.src) {
this.loading = true
this.speech = this.src
this.playAudio()
}
}
},
playAudio() {
let _ = this
this.audioContext = uni.createInnerAudioContext();
this.audioContext.src = this.speech;
this.audioContext.play()
this.audioContext.onPlay(() => {
_.text = "暂停"
});
this.audioContext.onPause(() => {
_.loading = false
_.text = "开始"
});
this.audioContext.onEnded(() => {
_.loading = false
_.text = "开始"
});
this.audioContext.onError((res) => {
console.error(res.errMsg);
_.text = "开始"
});
},
},
destroyed() {
if (this.audioContext) {
this.audioContext.pause()
this.audioContext.destroy()
}
}
}
</script>
<style lang="scss" scoped>
.AiTransSpeech {
display: flex;
align-items: center;
justify-content: center;
button {
position: fixed;
right: 80px;
bottom: 150px;
width: 104px;
height: 104px;
background: linear-gradient(130deg, rgba(70, 192, 253, 1) 0%, rgba(37, 158, 249, 1) 57%, rgba(39, 148, 248, 1) 100%);
border-radius: 50%;
font-size: 24px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff !important;
z-index: 10000;
& > div {
width: 48px;
white-space: nowrap;
line-height: normal;
.iconfont {
width: 48px;
font-size: 48px;
line-height: 48px;
overflow: hidden;
&.playing {
animation: playingSpeech .5s infinite;
}
}
}
}
@keyframes playingSpeech {
from {
width: 30px
}
to {
width: 100%
}
}
}
</style>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,226 @@
<template>
<div class="ai-uploader">
<div class="imgs flex-align">
<div class="img" v-for="(item, index) in fileList" :key="index">
<image :src="item.url" hover-class="text-hover" @click="prev(index)" class="image" />
<!-- <i class="iconfont" @click="remove(index)">&#xe6b3;</i> -->
<img src="https://cdn.cunwuyun.cn/dvcp/upload/del-icon.png" alt="" class="del-icon" @click="remove(index)"/>
</div>
<div class="upload" @click="upload" v-if="fileList.length < limit">
<!-- <i class="iconfont">&#xe6b2;</i> -->
<img src="https://cdn.cunwuyun.cn/dvcp/upload/add-icon.png" alt="" class="add-icon"/>
<span>添加照片</span>
</div>
</div>
</div>
</template>
<script>
import axios from '@/utils/axios.js'
export default {
name: 'AiUploader',
model: {
event: 'input',
prop: 'value',
},
props: {
limit: {
type: Number,
default: 1,
},
type: {
type: 'img',
},
value: {
type: Array,
default: () => [],
},
},
watch: {
value: {
handler(val) {
if (val) {
this.fileList = [...val]
}
},
immediate: true,
deep: true,
},
},
data() {
return {
fileList: [],
hideStatus: false,
}
},
methods: {
remove(index) {
this.fileList.splice(index, 1)
this.$nextTick(() => {
this.$emit('input', [...this.fileList])
this.$emit('change', [...this.fileList])
})
},
prev(index) {
uni.previewImage({
current: this.fileList[index].url,
urls: this.fileList.map((v) => v.url),
})
},
upload() {
uni.chooseImage({
count: this.limit,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
if (this.fileList.length + res.tempFilePaths.length > this.limit && this.limit !== 1) {
this.$toast(`图片不能超过${this.limit}`)
return false
}
this.$loading('上传中')
res.tempFilePaths.forEach((item, index) => {
if (index === res.tempFilePaths.length - 1) {
this.hideStatus = true
}
console.log(item)
this.uploadFile(item)
})
},
})
},
uploadFile(img) {
uni.uploadFile({
url: axios.baseURL + '/admin/file/add',
filePath: img,
name: 'file',
header: {
'Content-Type': 'multipart/form-data',
Authorization: uni.getStorageSync('token'),
},
success: (res) => {
const data = JSON.parse(res.data)
if (data.code === 0) {
if (this.limit === 1) {
this.fileList = [
{
id: data.data[0].split(';')[1],
url: data.data[0].split(';')[0],
},
]
} else {
this.fileList.push({
id: data.data[0].split(';')[1],
url: data.data[0].split(';')[0],
})
}
this.$nextTick(() => {
this.$emit('input', [...this.fileList])
this.$emit('change', [...this.fileList])
})
} else {
this.$toast(data.msg)
}
},
complete: () => {
if (this.hideStatus) {
this.$hideLoading()
this.hideStatus = false
}
},
})
},
},
}
</script>
<style lang="scss" scoped>
.ai-uploader {
.del-icon {
position: absolute;
right: 0;
top: 0;
z-index: 11;
width: 32px;
height: 32px;
-webkit-transform: translate(50%, -50%);
transform: translate(50%, -50%);
}
.imgs {
flex-wrap: wrap;
div {
position: relative;
width: 220px;
height: 220px;
margin-right: 8px;
margin-bottom: 8px;
&:nth-of-type(3n) {
margin-right: 0;
}
.image {
width: 220px;
height: 220px;
}
}
.img {
i {
position: absolute;
right: 0;
top: 0;
z-index: 11;
font-size: 28px;
opacity: 0.8;
color: #f72c27;
transform: translate(50%, -50%);
}
}
.upload {
width: 224px;
height: 224px;
text-align: center;
border: 1px solid #dddddd;
box-sizing: border-box;
i {
padding: 42px 0 16px;
color: #ddd;
font-size: 64px;
}
.add-icon {
margin: 50px 0 16px;
width: 64px;
height: 64px;
}
span {
display: block;
text-align: center;
color: #dddddd;
font-size: 24px;
}
}
}
}
</style>