夜良king 2023-01-10 18:27:52 +08:00
commit 4f243afb63
163 changed files with 17849 additions and 228 deletions

View File

@ -3,8 +3,8 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
common: "http://192.168.0.113:8890",
buyer: "http://192.168.0.113:8898",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
// common: "http://192.168.0.105:8890",

View File

@ -212,3 +212,52 @@ export function receiptSelect (params) {
params
});
}
/**
* 获取全部配送方式
*/
export function shippingMethodList(params) {
return request({
url: `/buyer/trade/carts/shippingMethodList`,
method: Method.GET,
needToken: true,
params: params,
});
}
/**
* 获取全部配送方式
*/
export function storeAddressList(params) {
return request({
url: `/buyer/store/address/shippingMethodList`,
method: Method.GET,
needToken: true,
params: params,
});
}
/**
* 设置自提地址ID
* @param addressId
*/
export function setStoreAddressId(storeAddressId,way) {
return request({
url: `/buyer/trade/carts/storeAddress?storeAddressId=${storeAddressId}&way=${way}`,
method: Method.GET,
needToken: true,
});
}
/**
* 提交配送方式
* @param params
*/
export function setShipMethod(params) {
return request({
url: "/buyer/trade/carts/shippingMethod",
method: Method.PUT,
needToken: true,
params,
});
}

View File

@ -75,3 +75,15 @@ export function applyStatus () {
method: Method.GET
})
}
/**
* 获取自提点信息
* @param storeId
*/
export function getStoreAddress(storeId,params) {
return request({
url: `/buyer/store/address/page/${storeId}`,
method: Method.GET,
params
});
}

View File

@ -8,12 +8,7 @@
<div class="shop-nav-container">
<Breadcrumb>
<BreadcrumbItem to="/">首页</BreadcrumbItem>
<BreadcrumbItem
v-for="(item, index) in categoryBar"
:to="goGoodsList(index)"
target="_blank"
:key="index"
>
<BreadcrumbItem v-for="(item, index) in categoryBar" :to="goGoodsList(index)" target="_blank" :key="index">
{{ item.name }}
</BreadcrumbItem>
</Breadcrumb>
@ -24,10 +19,7 @@
}}</router-link>
</span>
<span @click="collect">
<Icon
type="ios-heart"
:color="storeCollected ? '#ed3f14' : '#666'"
/>
<Icon type="ios-heart" :color="storeCollected ? '#ed3f14' : '#666'" />
{{ storeCollected ? "已收藏店铺" : "收藏店铺" }}
</span>
<span class="ml_10" @click="IMService()"></span>
@ -36,11 +28,7 @@
</div>
<!-- 商品信息展示 -->
<ShowGoods
@handleClickSku="targetClickSku"
v-if="goodsMsg.data"
:detail="goodsMsg"
></ShowGoods>
<ShowGoods @handleClickSku="targetClickSku" v-if="goodsMsg.data" :detail="goodsMsg"></ShowGoods>
<!-- 商品详细展示 -->
<ShowGoodsDetail v-if="goodsMsg.data" :detail="goodsMsg"></ShowGoodsDetail>
@ -98,7 +86,11 @@ export default {
"?token=" +
accessToken +
"&id=" +
this.goodsMsg.data.storeId
this.goodsMsg.data.storeId +
"&goodsId=" +
this.goodsMsg.data.goodsId +
"&skuId=" +
this.goodsMsg.data.id
);
},
// im

View File

@ -32,6 +32,10 @@ export const orderStatusList = [
{
name: '已取消',
status: 'CANCELLED'
},
{
name: '待提货',
status: 'STAY_PICKED_UP'
}
]
// 订单售后状态

View File

@ -53,7 +53,7 @@
></Step>
</Steps>
</div>
<div class="order-card">
<div class="order-card" v-if="order.order.deliveryMethod == 'LOGISTICS'">
<h3>收货人信息</h3>
<p>收货人{{ order.order.consigneeName }}</p>
<p>手机号码{{ order.order.consigneeMobile | secrecyMobile }}</p>
@ -62,6 +62,11 @@
{{ order.order.consigneeDetail }}
</p>
</div>
<div class="order-card" v-if="order.order.deliveryMethod == 'SELF_PICK_UP'">
<h3>自提点信息</h3>
<p>自提点名称{{ order.order.storeAddressPath }}</p>
<p>联系方式{{ order.order.storeAddressMobile }}</p>
</div>
<div class="order-card">
<h3>付款信息</h3>
<p>支付方式{{ order.paymentMethodValue }}</p>
@ -70,7 +75,7 @@
<div class="order-card" v-if="!order.order.verificationCode">
<h3>配送信息</h3>
<p>配送方式{{ order.deliveryMethodValue }}</p>
<p>配送状态{{ order.deliverStatusValue }}</p>
<p v-if="order.order.deliveryMethod === 'LOGISTICS'">{{ order.deliverStatusValue }}</p>
<p v-if="logistics">
物流信息{{ logistics.shipper || "暂无物流信息" }}
</p>
@ -270,6 +275,9 @@ export default {
if (res.success) {
this.order = res.result;
this.progressList = res.result.orderLogs;
if (this.order.order.deliveryMethod === 'LOGISTICS') {
this.traces();
}
}
});
},
@ -336,7 +344,6 @@ export default {
},
mounted() {
this.getDetail();
this.traces();
},
};
</script>

View File

@ -9,42 +9,29 @@
</div>
<div class="cart-steps">
<span :class="stepIndex == 1 ? 'active' : ''">1.我的购物车</span>
<Icon
:class="stepIndex == 1 ? 'active-arrow' : ''"
custom="icomoon icon-next"
></Icon>
<Icon :class="stepIndex == 1 ? 'active-arrow' : ''" custom="icomoon icon-next"></Icon>
<span :class="stepIndex == 1 ? 'active' : ''">2.填写订单信息</span>
<Icon
:class="stepIndex == 1 ? 'active-arrow' : ''"
custom="icomoon icon-next"
></Icon>
<Icon :class="stepIndex == 1 ? 'active-arrow' : ''" custom="icomoon icon-next"></Icon>
<span :class="stepIndex == 2 ? 'active' : ''">3.成功提交订单</span>
</div>
</div>
<Divider />
<div class="content width_1200">
<!-- 收货地址 -->
<div class="address">
<div class="address" v-if="selectedDeliverMethod === 'LOGISTICS'">
<div class="card-head">
<span>收货人信息</span>
<span @click="goAddressManage"></span>
</div>
<div class="address-manage">
<div
class="address-item"
v-show="moreAddr ? true : index < 3"
:class="selectedAddress.id === item.id ? 'border-red' : ''"
@mouseenter="showEditBtn = index"
@mouseleave="showEditBtn = ''"
@click="selectAddress(item)"
v-for="(item, index) in addressList"
:key="index"
>
<div class="address-item" v-show="moreAddr ? true : index < 3"
:class="selectedAddress.id === item.id ? 'border-red' : ''" @mouseenter="showEditBtn = index"
@mouseleave="showEditBtn = ''" @click="selectAddress(item)" v-for="(item, index) in addressList"
:key="index">
<div>
<span>{{ item.name }}</span>
<Tag class="ml_10" v-if="item.isDefault" color="red"></Tag>
<Tag class="ml_10" v-if="item.alias" color="warning"
>{{ item.alias }}
<Tag class="ml_10" v-if="item.alias" color="warning">{{ item.alias }}
</Tag>
</div>
<div>{{ item.mobile }}</div>
@ -53,12 +40,7 @@
</div>
<div class="edit-btn" v-show="showEditBtn === index">
<span @click.stop="editAddress(item.id)">修改</span>
<span
class="ml_10"
v-if="!item.isDefault"
@click.stop="delAddress(item)"
>删除</span
>
<span class="ml_10" v-if="!item.isDefault" @click.stop="delAddress(item)"></span>
</div>
<div class="corner-icon" v-show="selectedAddress.id === item.id">
<div></div>
@ -70,49 +52,81 @@
<div>添加新地址</div>
</div>
</div>
<div
class="more-addr"
@click="moreAddr = !moreAddr"
v-if="addressList.length > 3"
>
<div class="more-addr" @click="moreAddr = !moreAddr" v-if="addressList.length > 3">
{{ moreAddr ? "收起地址" : "更多地址" }}
<Icon v-show="!moreAddr" type="md-arrow-dropdown" />
<Icon v-show="moreAddr" type="md-arrow-dropup" />
</div>
</div>
<div class="address" v-if="selectedDeliverMethod === 'SELF_PICK_UP'">
<div class="card-head">
<span>自提点信息</span>
</div>
<div class="address-manage">
<div class="address-item" v-show="storeMoreAddr ? true : index < 3"
:class="selectedAddress.id === item.id ? 'border-red' : ''" @mouseenter="showEditBtn = index"
@mouseleave="showEditBtn = ''" @click="selectStoreAddress(item)" v-for="(item, index) in storeAddressList"
:key="index">
<div>
<span>{{ item.addressName }}</span>
</div>
<div>{{ item.mobile }}</div>
<div>
{{ item.address | unitAddress }} {{ item.detail }}
</div>
<div class="corner-icon" v-show="selectedStoreAddress.id === item.id">
<div></div>
<Icon type="md-checkmark" />
</div>
</div>
</div>
<div class="more-addr" @click="storeMoreAddr = !storeMoreAddr" v-if="addressList.length > 3">
{{ storeMoreAddr ? "收起地址" : "更多地址" }}
<Icon v-show="!storeMoreAddr" type="md-arrow-dropdown" />
<Icon v-show="storeMoreAddr" type="md-arrow-dropup" />
</div>
</div>
<div>
</div>
<div class="goods-content">
<div class="card-head mt_20 mb_20">
<span>配送方式</span>
</div>
<div class="delivery-method">
<div class="method-item" v-show="moreAddr ? true : index < 3"
:class="selectedDeliverMethod === item.value ? 'border-red' : ''" @mouseenter="showEditBtn = item.value"
@mouseleave="showEditBtn = ''" @click="selectDeliverMethod(item)" v-for="(item, index) in shippingMethod"
:key="index">
<div>{{ item.label }}</div>
<div class="corner-icon" v-show="selectedDeliverMethod === item.value">
<div></div>
<Icon type="md-checkmark" />
</div>
</div>
</div>
</div>
<!-- 商品信息 -->
<div class="goods-content">
<div class="card-head mt_20 mb_20">
<span>商品信息</span>
<span @click="$router.push('/cart')"></span>
</div>
<div
class="goods-msg"
v-for="(shop, shopIndex) in goodsList"
:key="shopIndex"
>
<div class="goods-msg" v-for="(shop, shopIndex) in goodsList" :key="shopIndex">
<div v-if="shop.checked">
<div class="shop-name">
<span>
<span class="hover-color" @click="goShopPage(shop.storeId)">{{
shop.storeName
}}</span
>&nbsp;&nbsp;
}}</span>&nbsp;&nbsp;
</span>
</div>
<div class="goods-list">
<div
class="goods-item"
v-for="(goods, goodsIndex) in shop.checkedSkuList"
:key="goodsIndex"
>
<span
class="hover-color"
@click="
<div class="goods-item" v-for="(goods, goodsIndex) in shop.checkedSkuList" :key="goodsIndex">
<span class="hover-color" @click="
goGoodsDetail(goods.goodsSku.id, goods.goodsSku.goodsId)
"
>
">
<img :src="goods.goodsSku.thumbnail" alt="" />
<span style="vertical-align: top">{{
goods.goodsSku.goodsName
@ -129,16 +143,8 @@
</div>
</div>
<div class="order-mark">
<Input
type="textarea"
maxlength="60"
v-model="shop.remark"
show-word-limit
placeholder="订单备注"
/>
<span style="font-size: 12px; color: #999"
>提示请勿填写有关支付收货发票方面的信息</span
>
<Input type="textarea" maxlength="60" v-model="shop.remark" show-word-limit placeholder="订单备注" />
<span style="font-size: 12px; color: #999">提示请勿填写有关支付收货发票方面的信息</span>
</div>
</div>
</div>
@ -146,13 +152,9 @@
<!-- 发票信息 -->
<div class="invoice">
<div class="card-head mt_20 mb_20">
<span class="relative"
>发票信息<span class="inv-tips">
<Icon
type="ios-alert-outline"
/>
</span></span
>
<span class="relative">发票信息<span class="inv-tips">
<Icon type="ios-alert-outline" />开企业抬头发票须填写纳税人识别号以免影响报销
</span></span>
</div>
<div class="inovice-content">
<span>{{ invoiceData.receiptTitle }}</span>
@ -167,47 +169,24 @@
</div>
<div v-if="couponList.length === 0"></div>
<ul v-else class="coupon-list">
<li
v-for="(item, index) in couponList"
class="coupon-item"
:key="index"
>
<li v-for="(item, index) in couponList" class="coupon-item" :key="index">
<div class="c-left">
<div>
<span
v-if="item.couponType === 'PRICE'"
class="fontsize_12 global_color"
><span class="price">{{
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{
item.price | unitPrice
}}</span></span
>
<span
v-if="item.couponType === 'DISCOUNT'"
class="fontsize_12 global_color"
><span class="price">{{ item.discount }}</span
></span
>
<span class="describe"
>{{ item.consumeThreshold }}元可用</span
>
}}</span></span>
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{
item.discount
}}</span></span>
<span class="describe">{{ item.consumeThreshold }}元可用</span>
</div>
<p>使用范围{{ useScope(item.scopeType) }}</p>
<p>有效期{{ item.endTime }}</p>
</div>
<img
class="used"
v-if="usedCouponId.includes(item.id)"
src="../../assets/images/geted.png"
alt=""
/>
<img class="used" v-if="usedCouponId.includes(item.id)" src="../../assets/images/geted.png" alt="" />
<b></b>
<a class="c-right" @click="useCoupon(item.id, true)">立即使用</a>
<a
class="c-right"
v-if="usedCouponId.includes(item.id)"
@click="useCoupon(item.id, false)"
>放弃优惠</a
>
<a class="c-right" v-if="usedCouponId.includes(item.id)" @click="useCoupon(item.id, false)"></a>
<i class="circle-top"></i>
<i class="circle-bottom"></i>
</li>
@ -216,29 +195,23 @@
<!-- 订单价格 -->
<div class="order-price">
<div>
<span>{{ totalNum }}件商品总商品金额</span
><span>{{ priceDetailDTO.goodsPrice | unitPrice("¥") }}</span>
<span>{{ totalNum }}件商品总商品金额</span><span>{{ priceDetailDTO.goodsPrice | unitPrice("¥") }}</span>
</div>
<div v-if="priceDetailDTO.freightPrice > 0">
<span>运费</span
><span>{{ priceDetailDTO.freightPrice | unitPrice("¥") }}</span>
<span>运费</span><span>{{ priceDetailDTO.freightPrice | unitPrice("¥") }}</span>
</div>
<div v-if="priceDetailDTO.discountPrice > 0">
<span>优惠金额</span
><span>-{{ priceDetailDTO.discountPrice | unitPrice("¥") }}</span>
<span>优惠金额</span><span>-{{ priceDetailDTO.discountPrice | unitPrice("¥") }}</span>
</div>
<div v-if="priceDetailDTO.couponPrice > 0">
<span>优惠券金额</span
><span>-{{ priceDetailDTO.couponPrice | unitPrice("¥") }}</span>
<span>优惠券金额</span><span>-{{ priceDetailDTO.couponPrice | unitPrice("¥") }}</span>
</div>
<div v-if="$route.query.way === 'POINTS'">
<span>应付积分</span
><span class="actrual-price">{{ priceDetailDTO.payPoint }}</span>
<span>应付积分</span><span class="actrual-price">{{ priceDetailDTO.payPoint }}</span>
</div>
<div v-else>
<span>应付金额</span
><span class="actrual-price">{{
<span>应付金额</span><span class="actrual-price">{{
priceDetailDTO.flowPrice | unitPrice("¥")
}}</span>
</div>
@ -256,17 +229,9 @@
</div>
<BaseFooter></BaseFooter>
<!-- 添加发票模态框 -->
<invoice-modal
ref="invModal"
:invoiceData="invoiceData"
@change="getInvMsg"
/>
<invoice-modal ref="invModal" :invoiceData="invoiceData" @change="getInvMsg" />
<!-- 选择地址模态框 -->
<address-manage
ref="address"
:id="addrId"
@change="addrChange"
></address-manage>
<address-manage ref="address" :id="addrId" @change="addrChange"></address-manage>
</div>
</template>
@ -279,8 +244,12 @@ import {
createTrade,
selectAddr,
selectCoupon,
setShipMethod,
setStoreAddressId,
shippingMethodList,
couponNum,
} from "@/api/cart";
import { getStoreAddress } from "@/api/shopentry.js"
import { canUseCouponList } from "@/api/member.js";
export default {
@ -288,15 +257,35 @@ export default {
components: { invoiceModal, addressManage },
data() {
return {
selectedStoreAddress: 'm',
selectMethod: '',
stepIndex: 1, //
invoiceAvailable: false, //
showEditBtn: "", //
orderMark: "", //
storeMoreAddr: false,
invoiceData: {
//
receiptTitle: "个人",
receiptContent: "不开发票",
},
searchForm: {
pageNumber: 1,
pageSize: 100
},
shippingMethod: [],
storeAddressList: [],
shippingWay: [
{
value: "LOGISTICS",
label: "物流",
},
{
value: "SELF_PICK_UP",
label: "自提",
},
],
selectedDeliverMethod: 'LOGISTICS',
addressList: [], //
selectedAddress: {}, //
goodsList: [], //
@ -308,6 +297,7 @@ export default {
couponList: [], //
usedCouponId: [], // 使id
selectedCoupon: {}, //
storeId: '', //Id
};
},
mounted() {
@ -317,6 +307,7 @@ export default {
//
init() {
this.getGoodsDetail();
this.getDistribution();
},
goAddressManage() {
//
@ -324,7 +315,7 @@ export default {
},
getAddress() {
//
memberAddress().then((res) => {
memberAddress(this.searchForm).then((res) => {
if (res.success) {
this.addressList = res.result.records;
this.addressList.forEach((e, index) => {
@ -335,6 +326,38 @@ export default {
}
});
},
//
async getDistribution() {
let shopRes = await shippingMethodList({ way: this.$route.query.way });
let shopList;
if (shopRes.success) {
shopList = shopRes.result;
let way = [];
console.log(shopList)
this.shippingWay.forEach((item) => {
shopList.forEach((child) => {
if (item.value == child) {
way.push(item);
}
});
});
this.shippingMethod = way;
console.log(this.shippingMethod)
}
},
async getStoreAddressList() {
getStoreAddress(this.storeId,this.searchForm).then(res => {
if (res.success) {
this.storeAddressList = res.result.records
this.storeAddressList.forEach((e, index) => {
if (e.id === this.selectedAddress.id && index > 2) {
this.storeMoreAddr = true;
}
});
}
})
},
getGoodsDetail() {
//
this.$Spin.show();
@ -364,7 +387,7 @@ export default {
this.goodsList = res.result.cartList;
this.priceDetailDTO = res.result.priceDetailDTO;
this.skuList = res.result.skuList;
this.storeId = this.goodsList[0].storeId
if (res.result.receiptVO) {
this.invoiceData = res.result.receiptVO;
}
@ -401,6 +424,7 @@ export default {
this.selectedAddress = res.result.memberAddress;
}
this.getAddress();
this.getStoreAddressList();
this.totalNum = 0;
for (let i = 0; i < this.skuList.length; i++) {
this.totalNum += this.skuList[i].num;
@ -475,8 +499,32 @@ export default {
};
selectAddr(params).then((res) => {
if (res.success) {
this.$Message.success("选择收货地址成功");
this.selectedAddress = item;
this.$Message.success("选择配送方式成功");
this.selectMethod = item;
this.getGoodsDetail();
}
});
},
selectStoreAddress(item) {
console.log(item.id)
console.log(this.$route.query.way)
//
setStoreAddressId(item.id,this.$route.query.way).then((res) => {
if (res.success) {
this.$Message.success("选择自提地址成功");
this.selectedStoreAddress = item;
this.getGoodsDetail();
}
});
},
selectDeliverMethod(item) {
let params = {
way: this.$route.query.way,
shippingMethod: item.value,
};
setShipMethod(params).then((res) => {
if (res.success) {
this.selectedDeliverMethod = item.value;
this.getGoodsDetail();
}
});
@ -605,9 +653,11 @@ export default {
<style scoped lang="scss">
@import "../../assets/styles/coupon.scss";
.goods-msg {
overflow: hidden;
}
/** logo start */
.logo {
height: 40px;
@ -680,6 +730,76 @@ export default {
padding: 15px 25px;
}
.delivery-method {
display: flex;
flex-wrap: wrap;
>div {
border: 1px dotted #949494;
width: 50px;
height: 40px;
margin: 20px 20px 0 0;
padding: 10px;
cursor: pointer;
color: #999;
}
.method-item {
position: relative;
font-size: 12px;
>div:nth-child(1) {
margin-bottom: 10px;
span {
margin-right: 10px;
}
>span:nth-child(1) {
color: #000000;
font-size: 14px;
}
}
.edit-btn {
font-size: 12px;
position: absolute;
top: 15px;
right: 20px;
color: $theme_color;
span:hover {
border-bottom: 1px solid $theme_color;
}
}
.corner-icon {
position: absolute;
right: -1px;
bottom: -1px;
div {
width: 0;
border-top: 20px solid transparent;
border-right: 20px solid $theme_color;
}
.ivu-icon {
font-size: 12px;
position: absolute;
bottom: 0;
right: 1px;
transform: rotate(-15deg);
color: #fff;
}
}
}
.border-red {
border-color: $theme_color;
}
}
/** 地址管理 */
.address-manage {
display: flex;
@ -704,6 +824,7 @@ export default {
.ivu-icon {
font-size: 24px;
}
}
.address-item {
@ -843,6 +964,7 @@ export default {
flex: 1;
text-align: left;
>span {
margin-left: 10px;
}

7
im/.env Normal file
View File

@ -0,0 +1,7 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=https://im-api.pickmall.cn
VUE_APP_WEB_SOCKET_URL=wss://im-api.pickmall.cn/lili/webSocket
VUE_APP_COMMON=https://common-api.pickmall.cn
VUE_APP_WEBSITE_NAME="LiLi IM"
VUE_APP_PC_ORDER_URL=https://store-b2b2c.pickmall.cn

7
im/.env.development Normal file
View File

@ -0,0 +1,7 @@
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=http://192.168.0.113:8885
VUE_APP_WEB_SOCKET_URL=ws://192.168.0.113:8885/lili/webSocket
VUE_APP_COMMON=http://192.168.0.113:8890
VUE_APP_PC_URL="http://192.168.0.113:10001"
VUE_APP_WEBSITE_NAME="LiLi IM"

23
im/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

21
im/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 YuanDong
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

71
im/README.md Normal file
View File

@ -0,0 +1,71 @@
# Springboot-websocket
过渡阶段IM
前端仓库https://gitee.com/beijing_hongye_huicheng/im
后端仓库https://gitee.com/beijing_hongye_huicheng/springboot-websocket
部署方式:
1、导入数据库配置resource目录下application.yml的数据库以及redis配置文件。
2、maven打包
3、启动jar包即可。
4、前端程序运行测试环境需配置根目录 .env.development文件正式环境需配置 .env 文件。
# 参考项目
项目前端参考https://gitee.com/gzydong/LumenIM.git 功能更丰富,大家可以去学习一波
# IM体验
浏览器1打开地址http://127.0.0.1:8000/message?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyQ29udGV4dCI6IntcInVzZXJuYW1lXCI6XCIxMzAxMTExMTExMVwiLFwibmlja05hbWVcIjpcIuW8oOS4ieWTiOWTiOWTiFwiLFwiZmFjZVwiOlwiaHR0cHM6Ly9saWxpc2hvcC1vc3Mub3NzLWNuLWJlaWppbmcuYWxpeXVuY3MuY29tLzQ1ZDUyOGYwMjRjZTQyMzI4NzYxNjFhZmQxN2Y0ZWExLmpwZ1wiLFwiaWRcIjpcIjEzNzY0MTc2ODQxNDAzMjY5MTJcIixcImxvbmdUZXJtXCI6ZmFsc2UsXCJyb2xlXCI6XCJTVE9SRVwiLFwic3RvcmVJZFwiOlwiMTM3NjQzMzU2NTI0NzQ3MTYxNlwiLFwic3RvcmVOYW1lXCI6XCLlrrblrrbkuZBcIixcImlzU3VwZXJcIjpmYWxzZX0iLCJzdWIiOiIxMzAxMTExMTExMSIsImV4cCI6MTY0NTQ2OTk1M30.fXpCZ2YiFYqACpmxVvKjIpXovfPRDJavHjftpQzdEps
浏览器2打开地址http://127.0.0.1:8000/message?token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyQ29udGV4dCI6IntcInVzZXJuYW1lXCI6XCIxMzAxMTExMTExMVwiLFwibmlja05hbWVcIjpcIuW8oOS4ieWTiOWTiOWTiFwiLFwiZmFjZVwiOlwiaHR0cHM6Ly9saWxpc2hvcC1vc3Mub3NzLWNuLWJlaWppbmcuYWxpeXVuY3MuY29tLzk2N2UzMzU1Yzg0NTRiNGFhMTk1N2M1NTQ5ZTZiNzIwLnBuZ1wiLFwiaWRcIjpcIjEzNzY0MTc2ODQxNDAzMjY5MTJcIixcImxvbmdUZXJtXCI6ZmFsc2UsXCJyb2xlXCI6XCJNRU1CRVJcIixcImlzU3VwZXJcIjpmYWxzZX0iLCJzdWIiOiIxMzAxMTExMTExMSIsImV4cCI6MTY0NTQ2OTAyNn0.GEkGpKRKF3rqzHRhaPCFilPpWe37cIXTT4KnwWR4Bt0&id=1376433565247471616
即可进行聊天。
# NGINX配置事例
线上NGINX配置在测试阶段出现问题这里吧相关的配置贴出来供大家参考。
````
server {
listen 443 ssl;
ssl_certificate "/etc/nginx/ssl/pickmall.cn.pem";
ssl_certificate_key "/etc/nginx/ssl/pickmall.cn.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
include /etc/nginx/default.d/*.conf;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
server_name im-api.pickmall.cn;
location / {
proxy_pass http://127.0.0.1:8088;
}
}
server {
listen 443 ssl;
ssl_certificate "/etc/nginx/ssl/pickmall.cn.pem";
ssl_certificate_key "/etc/nginx/ssl/pickmall.cn.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
include /etc/nginx/default.d/*.conf;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
server_name im.pickmall.cn;
try_files $uri $uri/ /index.html;
root /home/im/im/dist;
}
````

51
im/babel.config.js Normal file
View File

@ -0,0 +1,51 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
[
"import",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
],
[
"prismjs",
{
"languages": [
"html",
"css",
"less",
"javascript",
"typescript",
"json",
"xml",
"bash",
"nginx",
"sql",
"docker",
"php",
"java",
"go",
"python",
"ruby",
"rust",
"objectivec",
"c",
"csharp",
"cpp",
"lua",
"shell",
"vim",
"yaml",
"yml",
"md",
"erlang",
"ini"
],
"theme": "okaidia"
}
]
]
}

18
im/jsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"src/**/*"
]
}

67
im/package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "LiLi-IM",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^0.21.4",
"babel-plugin-prismjs": "^2.0.1",
"core-js": "^3.6.5",
"element-ui": "^2.14.1",
"js-audio-recorder": "^1.0.6",
"js-base64": "^2.5.1",
"mavon-editor": "^2.10.4",
"nprogress": "^0.2.0",
"prismjs": "^1.29.0",
"svg-sprite-loader": "^5.0.0",
"vue": "^2.6.11",
"vue-contextmenujs": "^1.3.13",
"vue-cropper": "^0.5.5",
"vue-prism-editor": "^0.5.1",
"vue-router": "^3.4.9",
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"babel-eslint": "^10.1.0",
"babel-plugin-import": "^1.13.1",
"compression-webpack-plugin": "^5.0.0",
"eslint": "^8.28.0",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"postcss": "^8.4.20",
"style-resources-loader": "^1.4.1",
"vue-cli-plugin-style-resources-loader": "^0.1.4",
"vue-svg-component-runtime": "^1.0.1",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.11",
"webpack": "^5.75.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

BIN
im/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

34
im/public/index.html Normal file
View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover">
<link rel="icon" href="favicon.ico">
<title>LiLi IM</title>
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
<link rel="stylesheet" href="https://at.alicdn.com/t/font_1425251_3v0kq1by4iq.css">
<link rel="stylesheet" href="https://cdn.bootcss.com/animate.css/3.7.2/animate.css">
</head>
<body>
<noscript>
<strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">
<div class="first-loading-wrp">
<div class="loading-wrp">
<span class="dot dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script type="text/javascript" src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>

47
im/src/App.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<div id="app">
<router-view v-if="showView" />
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
// router
showView: true,
beforeUnload: '',
Handler: ''
}
},
methods: {
//
refreshView () {
this.showView = false
this.$nextTick(() => (this.showView = true))
},
beforeunloadHandler () {
this.beforeUnload = new Date().getTime();
},
// btnHide
unloadHandler (e) {
this.Handler = new Date().getTime() - this.beforeUnload;
if (this.Handler <= 5) {
var storage = window.localStorage;
storage.clear()
localStorage.setItem('btnHide', 1)
}
},
},
mounted () {
window.addEventListener('beforeunload', e => this.beforeunloadHandler(e))
window.addEventListener('unload', e => this.unloadHandler(e))
},
destroyed () {
window.removeEventListener('beforeunload', e => this.beforeunloadHandler(e))
window.removeEventListener('unload', e => this.unloadHandler(e))
},
}
</script>

109
im/src/api/chat.js Normal file
View File

@ -0,0 +1,109 @@
import { post, get, upload, del } from "@/utils/request";
// 获取聊天列表服务接口
export const ServeGetTalkList = (data) => {
return get("/im/talk/list", data);
};
// 获取聊天列表服务接口
export const ServeGetStoreTalkList = (data) => {
return get("/im/talk/store/list", data);
};
// 聊天列表创建服务接口
export const ServeCreateTalkList = (id) => {
return get(`/im/talk/user/${id}`);
};
// 删除聊天列表服务接口
export const ServeDeleteTalkList = (data) => {
return del("/im/talk", data);
};
// 对话列表置顶服务接口
export const ServeTopTalkList = (data) => {
return post("/im/talk/top", data);
};
// 清除聊天消息未读数服务接口
export const ServeClearTalkUnreadNum = (data) => {
return post("/im/talk/update-unread-num", data);
};
// 获取聊天记录服务接口
export const ServeTalkRecords = (data) => {
return get("/im/message", data);
};
// 获取转发会话记录详情列表服务接口
export const ServeGetForwardRecords = (data) => {
return get("/im/talk/get-forward-records", data);
};
// 对话列表置顶服务接口
export const ServeSetNotDisturb = (data) => {
return post("/im/talk/disturb", data);
};
// 查找用户聊天记录服务接口
export const ServeFindTalkRecords = (data) => {
return get("/im/talk/find-chat-records", data);
};
// 搜索用户聊天记录服务接口
export const ServeSearchTalkRecords = (data) => {
return get("/im/talk/search-chat-records", data);
};
export const ServeGetRecordsContext = (data) => {
return get("/im/talk/get-records-context", data);
};
// 发送代码块消息服务接口
export const ServeSendTalkCodeBlock = (data) => {
return post("/im/talk/message/code", data);
};
// 发送聊天文件服务接口
export const ServeSendTalkFile = (data) => {
return post("/im/talk/message/file", data);
};
// 发送聊天图片服务接口
export const ServeSendTalkImage = (data) => {
return upload("/common/common/upload/file", data);
};
// 发送表情包服务接口
export const ServeSendEmoticon = (data) => {
return post("/im/talk/message/emoticon", data);
};
// 转发消息服务接口
export const ServeForwardRecords = (data) => {
return post("/im/talk/message/forward", data);
};
// 撤回消息服务接口
export const ServeRevokeRecords = (data) => {
return post("/im/talk/message/revoke", data);
};
// 删除消息服务接口
export const ServeRemoveRecords = (data) => {
return post("/im/talk/message/delete", data);
};
// 收藏表情包服务接口
export const ServeCollectEmoticon = (data) => {
return post("/im/talk/message/collect", data);
};
//投票
export const ServeSendVote = (data) => {
return post("/im/talk/message/vote", data);
};
export const ServeConfirmVoteHandle = (data) => {
return post("/im/talk/message/vote/handle", data);
};

45
im/src/api/contacts.js Normal file
View File

@ -0,0 +1,45 @@
import { post, get } from '@/utils/request'
// 获取好友列表服务接口
export const ServeGetContacts = data => {
return get('/contacts/list', data)
}
// 解除好友关系服务接口
export const ServeDeleteContact = data => {
return post('/contacts/delete', data)
}
// 修改好友备注服务接口
export const ServeEditContactRemark = data => {
return post('/contacts/edit-remark', data)
}
// 搜索联系人
export const ServeSearchContact = data => {
return get('/contacts/search', data)
}
// 好友申请服务接口
export const ServeCreateContact = data => {
return post('/contacts/apply/create', data)
}
// 查询好友申请服务接口
export const ServeGetContactApplyRecords = data => {
return get('/contacts/apply/records', data)
}
// 处理好友申请服务接口
export const ServeApplyAccept = data => {
return post('/contacts/apply/accept', data)
}
export const ServeApplyDecline = data => {
return post('/contacts/apply/decline', data)
}
// 查询好友申请未读数量服务接口
export const ServeFindFriendApplyNum = () => {
return get('/contacts/apply-unread-num')
}

26
im/src/api/emoticon.js Normal file
View File

@ -0,0 +1,26 @@
import { post, get, upload } from '@/utils/request'
// 查询用户表情包服务接口
export const ServeFindUserEmoticon = () => {
return get('/emoticon/list')
}
// 查询系统表情包服务接口
export const ServeFindSysEmoticon = () => {
return get('/emoticon/system')
}
// 设置用户表情包服务接口
export const ServeSetUserEmoticon = data => {
return post('/emoticon/set-user-emoticon', data)
}
// 移除收藏表情包服务接口
export const ServeDelCollectEmoticon = data => {
return post('/emoticon/del-collect-emoticon', data)
}
// 上传表情包服务接口
export const ServeUploadEmoticon = data => {
return upload('/emoticon/upload-emoticon', data)
}

5
im/src/api/goods.js Normal file
View File

@ -0,0 +1,5 @@
import { post, get, upload, del } from "@/utils/request";
export const ServeGetGoodsDetail = (data) => {
return get(`/im/goods/goods/sku/${data.goodsId}/${data.skuId}`);
};

66
im/src/api/group.js Normal file
View File

@ -0,0 +1,66 @@
import { post, get } from '@/utils/request'
// 查询用户群聊服务接口
export const ServeGetGroups = () => {
return get('/group/list')
}
// 获取群信息服务接口
export const ServeGroupDetail = data => {
return get('/group/detail', data)
}
// 创建群聊服务接口
export const ServeCreateGroup = data => {
return post('/group/create', data)
}
// 修改群信息
export const ServeEditGroup = data => {
return post('/group/edit', data)
}
// 邀请好友加入群聊服务接口
export const ServeInviteGroup = data => {
return post('/group/invite', data)
}
// 移除群聊成员服务接口
export const ServeRemoveMembersGroup = data => {
return post('/group/remove-members', data)
}
// 管理员解散群聊服务接口
export const ServeDismissGroup = data => {
return post('/group/dismiss', data)
}
// 用户退出群聊服务接口
export const ServeSecedeGroup = data => {
return post('/group/secede', data)
}
// 修改群聊名片服务接口
export const ServeUpdateGroupCard = data => {
return post('/group/set-group-card', data)
}
// 获取用户可邀请加入群组的好友列表
export const ServeGetInviteFriends = data => {
return get('/group/invite-friends', data)
}
// 获取群组成员列表
export const ServeGetGroupMembers = data => {
return get('/group/members', data)
}
// 获取群组公告列表
export const ServeGetGroupNotices = data => {
return get('/group/notices', data)
}
// 编辑群公告
export const ServeEditGroupNotice = data => {
return post('/group/edit-notice', data)
}

16
im/src/api/upload.js Normal file
View File

@ -0,0 +1,16 @@
import { post, get, upload } from '@/utils/request'
// 上传头像裁剪图片服务接口
export const ServeUploadFileStream = data => {
return post('/upload/file-stream', data)
}
// 查询大文件拆分信息服务接口
export const ServeFindFileSplitInfo = (data = {}) => {
return get('/upload/get-file-split-info', data)
}
// 文件拆分上传服务接口
export const ServeFileSubareaUpload = (data = {}, options = {}) => {
return upload('/upload/file-subarea-upload', data, options)
}

30
im/src/api/user.js Normal file
View File

@ -0,0 +1,30 @@
import { get } from "@/utils/request";
// 获取用户相关设置信息
export const ServeGetUserSetting = () => {
return get("/im/user");
};
// 获取店铺相关设置信息
export const ServeGetStoreSetting = () => {
return get("/im/user/store");
};
// 获取用户相关设置信息
export const ServeGetUserDetail = (memberId) => {
return get(`/im/user/${memberId}`);
};
// 获取店铺相关设置信息
export const ServeGetStoreDetail = (storeId) => {
return get(`/im/user/store/${storeId}`);
};
// 获取店铺相关设置信息
export const ServeGetFootPrint = (params) => {
return get(`/im/user/history`,params);
};
// 获取订单列表信息
export const ServeGetOrderPrint = (params) => {
return get(`/im/orders/orders`,params);
};

View File

@ -0,0 +1,238 @@
@import './reset.css';
.no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.no-padding {
padding: 0;
}
.avatar-box {
height: 35px;
width: 35px;
flex-shrink: 0;
background-color: #508afe;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: white;
user-select: none;
transition: ease 1s;
position: relative;
overflow: hidden;
img {
width: 100%;
height: 100%;
background-color: white;
border-radius: 3px;
}
.top-mask {
width: 100%;
height: 100%;
background-color: rgba(22, 25, 29, 0.6);
position: absolute;
top: 0;
left: 0;
color: white;
display: none;
align-items: center;
justify-content: center;
font-weight: bold;
}
&:hover .top-mask {
display: flex;
}
}
.no-border {
border: 0;
}
.pointer {
cursor: pointer;
}
.border-radius0 {
border-radius: 0;
}
.full-height {
height: 100%;
}
.talk-height {
height: 80%;
width: 80%;
}
.ov-hidden {
overflow: hidden;
}
// 滚动条样式
.lum-scrollbar {
&::-webkit-scrollbar {
width: 3px;
background-color: #e4e4e5;
}
&::-webkit-scrollbar-thumb {
border-radius: 3px;
background-color: #c0bebc;
}
}
.larkc-tag {
font-size: 12px;
font-weight: 400;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 6px;
height: 20px;
border-radius: 2px;
cursor: default;
user-select: none;
background-color: #dee0e3;
transform: scale(0.83);
transform-origin: left;
flex-shrink: 0;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
// 自定义 dialog 样式
// lum-dialog -- start
.lum-dialog-mask {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
background-color: @maskBagColor;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
.lum-dialog-box {
min-width: 200px;
min-height: 200px;
background-color: white;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 2px 8px 0 rgba(31, 35, 41, 0.2);
margin: 0 10px;
.container {
height: 100%;
}
.header {
padding: 0;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f5eeee;
>p:first-child {
text-indent: 20px;
}
.tools {
height: 100%;
width: 100px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 20px;
i {
font-size: 20px;
cursor: pointer;
margin-left: 8px;
}
}
}
.main {
/deep/.el-input__inner {
border-radius: 1px !important;
}
.submit-btn {
border-radius: 2px;
font-weight: 400;
}
}
}
}
// lum-dialog -- end
.talk-notify {
.el-notification__title {
font-weight: 300;
font-size: 16px;
color: #f44336;
}
p {
max-height: 65px;
overflow: hidden;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-indent: -7px;
word-break: break-all;
}
}
.im-notify {
padding: 14px 26px 8px 0px;
.el-notification__closeBtn {
position: absolute;
top: 12px;
}
.el-notification__group {
margin-left: 5px;
}
}
.flex {
display: flex;
}
.flex-2 {
flex: 2;
}
.flex-8 {
flex: 8;
}
.flex-4 {
flex: 4;
}
.flex-10 {
flex: 10;
}

View File

@ -0,0 +1,991 @@
.markdown-body .octicon {
display: inline-block;
fill: currentColor;
vertical-align: text-bottom;
}
.markdown-body .anchor {
float: left;
line-height: 1;
margin-left: -20px;
padding-right: 4px;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #1b1f23;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
width: 16px;
height: 16px;
content: ' ';
display: inline-block;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E");
}
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
font-size: 16px;
word-wrap: break-word;
font-family: Content-font, Roboto, sans-serif;
font-weight: 400;
color: #3B454E;
line-height: 1.625;
}
.markdown-body details {
display: block;
}
.markdown-body summary {
display: list-item;
}
.markdown-body a {
background-color: initial;
}
.markdown-body a:active,
.markdown-body a:hover {
outline-width: 0;
}
.markdown-body strong {
font-weight: inherit;
font-weight: bolder;
}
.markdown-body h1 {
font-size: 2em;
margin: .67em 0;
}
.markdown-body img {
border-style: none;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body hr {
box-sizing: initial;
height: 0;
overflow: visible;
}
.markdown-body input {
font: inherit;
margin: 0;
}
.markdown-body input {
overflow: visible;
}
.markdown-body [type=checkbox] {
box-sizing: border-box;
padding: 0;
}
.markdown-body * {
box-sizing: border-box;
}
.markdown-body input {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body a {
color: #0366d6;
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body strong {
font-weight: 600;
}
.markdown-body hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #dfe2e5;
}
.markdown-body hr:after,
.markdown-body hr:before {
display: table;
content: "";
}
.markdown-body hr:after {
clear: both;
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body details summary {
cursor: pointer;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: 1px solid #d1d5da;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #d1d5da;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body h1 {
font-size: 32px;
}
.markdown-body h1,
.markdown-body h2 {
font-weight: 600;
}
.markdown-body h2 {
font-size: 24px;
}
.markdown-body h3 {
font-size: 20px;
}
.markdown-body h3,
.markdown-body h4 {
font-weight: 600;
}
.markdown-body h4 {
font-size: 16px;
}
.markdown-body h5 {
font-size: 14px;
}
.markdown-body h5,
.markdown-body h6 {
font-weight: 600;
}
.markdown-body h6 {
font-size: 12px;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 20px 0 !important;
background-color: #f5f8fc;
padding: 1rem;
color: #8796a8;
border-left: none;
}
.markdown-body ol,
.markdown-body ul {
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ol ol ol,
.markdown-body ol ul ol,
.markdown-body ul ol ol,
.markdown-body ul ul ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body code,
.markdown-body pre {
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body input::-webkit-inner-spin-button,
.markdown-body input::-webkit-outer-spin-button {
margin: 0;
-webkit-appearance: none;
appearance: none;
}
.markdown-body :checked+.radio-label {
position: relative;
z-index: 1;
border-color: #0366d6;
}
.markdown-body .border {
border: 1px solid #e1e4e8 !important;
}
.markdown-body .border-0 {
border: 0 !important;
}
.markdown-body .border-bottom {
border-bottom: 1px solid #e1e4e8 !important;
}
.markdown-body .rounded-1 {
border-radius: 3px !important;
}
.markdown-body .bg-white {
background-color: #fff !important;
}
.markdown-body .bg-gray-light {
background-color: #fafbfc !important;
}
.markdown-body .text-gray-light {
color: #6a737d !important;
}
.markdown-body .mb-0 {
margin-bottom: 0 !important;
}
.markdown-body .my-2 {
margin-top: 8px !important;
margin-bottom: 8px !important;
}
.markdown-body .pl-0 {
padding-left: 0 !important;
}
.markdown-body .py-0 {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.markdown-body .pl-1 {
padding-left: 4px !important;
}
.markdown-body .pl-2 {
padding-left: 8px !important;
}
.markdown-body .py-2 {
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.markdown-body .pl-3,
.markdown-body .px-3 {
padding-left: 16px !important;
}
.markdown-body .px-3 {
padding-right: 16px !important;
}
.markdown-body .pl-4 {
padding-left: 24px !important;
}
.markdown-body .pl-5 {
padding-left: 32px !important;
}
.markdown-body .pl-6 {
padding-left: 40px !important;
}
.markdown-body .f6 {
font-size: 12px !important;
}
.markdown-body .lh-condensed {
line-height: 1.25 !important;
}
.markdown-body .text-bold {
font-weight: 600 !important;
}
.markdown-body .pl-c {
color: #6a737d;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #005cc5;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #6f42c1;
}
.markdown-body .pl-s .pl-s1,
.markdown-body .pl-smi {
color: #24292e;
}
.markdown-body .pl-ent {
color: #22863a;
}
.markdown-body .pl-k {
color: #d73a49;
}
.markdown-body .pl-pds,
.markdown-body .pl-s,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sra,
.markdown-body .pl-sr .pl-sre {
color: #032f62;
}
.markdown-body .pl-smw,
.markdown-body .pl-v {
color: #e36209;
}
.markdown-body .pl-bu {
color: #b31d28;
}
.markdown-body .pl-ii {
color: #fafbfc;
background-color: #b31d28;
}
.markdown-body .pl-c2 {
color: #fafbfc;
background-color: #d73a49;
}
.markdown-body .pl-c2:before {
content: "^M";
}
.markdown-body .pl-sr .pl-cce {
font-weight: 700;
color: #22863a;
}
.markdown-body .pl-ml {
color: #735c0f;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: 700;
color: #005cc5;
}
.markdown-body .pl-mi {
font-style: italic;
color: #24292e;
}
.markdown-body .pl-mb {
font-weight: 700;
color: #24292e;
}
.markdown-body .pl-md {
color: #b31d28;
background-color: #ffeef0;
}
.markdown-body .pl-mi1 {
color: #22863a;
background-color: #f0fff4;
}
.markdown-body .pl-mc {
color: #e36209;
background-color: #ffebda;
}
.markdown-body .pl-mi2 {
color: #f6f8fa;
background-color: #005cc5;
}
.markdown-body .pl-mdr {
font-weight: 700;
color: #6f42c1;
}
.markdown-body .pl-ba {
color: #586069;
}
.markdown-body .pl-sg {
color: #959da5;
}
.markdown-body .pl-corl {
text-decoration: underline;
color: #032f62;
}
.markdown-body .mb-0 {
margin-bottom: 0 !important;
}
.markdown-body .my-2 {
margin-bottom: 8px !important;
}
.markdown-body .my-2 {
margin-top: 8px !important;
}
.markdown-body .pl-0 {
padding-left: 0 !important;
}
.markdown-body .py-0 {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.markdown-body .pl-1 {
padding-left: 4px !important;
}
.markdown-body .pl-2 {
padding-left: 8px !important;
}
.markdown-body .py-2 {
padding-top: 8px !important;
padding-bottom: 8px !important;
}
.markdown-body .pl-3 {
padding-left: 16px !important;
}
.markdown-body .pl-4 {
padding-left: 24px !important;
}
.markdown-body .pl-5 {
padding-left: 32px !important;
}
.markdown-body .pl-6 {
padding-left: 40px !important;
}
.markdown-body .pl-7 {
padding-left: 48px !important;
}
.markdown-body .pl-8 {
padding-left: 64px !important;
}
.markdown-body .pl-9 {
padding-left: 80px !important;
}
.markdown-body .pl-10 {
padding-left: 96px !important;
}
.markdown-body .pl-11 {
padding-left: 112px !important;
}
.markdown-body .pl-12 {
padding-left: 128px !important;
}
.markdown-body hr {
border-bottom-color: #eee;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: 1px solid #d1d5da;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #d1d5da;
}
.markdown-body:after,
.markdown-body:before {
display: table;
content: "";
}
.markdown-body:after {
clear: both;
}
.markdown-body>:first-child {
margin-top: 0 !important;
}
.markdown-body>:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body blockquote,
.markdown-body details,
.markdown-body dl,
.markdown-body ol,
.markdown-body p,
.markdown-body pre,
.markdown-body table,
.markdown-body ul {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body hr {
height: .25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.markdown-body blockquote {
margin: 20px 0 !important;
background-color: #f5f8fc;
padding: 1rem;
color: #8796a8;
border-left: 3px solid #03A9F4;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 {
font-size: 2em;
}
.markdown-body h1,
.markdown-body h2 {
padding-bottom: .3em;
/* border-bottom: 1px solid #eaecef; */
}
.markdown-body h2 {
font-size: 1.5em;
}
.markdown-body h3 {
font-size: 1.25em;
}
.markdown-body h4 {
font-size: 1em;
}
.markdown-body h5 {
font-size: .875em;
}
.markdown-body h6 {
font-size: .85em;
color: #6a737d;
}
.markdown-body ol,
.markdown-body ul {
padding-left: 2em;
}
.markdown-body ol ol,
.markdown-body ol ul,
.markdown-body ul ol,
.markdown-body ul ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li {
word-wrap: break-all;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body li+li {
margin-top: .25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table {
display: block;
width: 100%;
overflow: auto;
}
.markdown-body table th {
font-weight: 600;
}
.markdown-body table td,
.markdown-body table th {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body img {
max-width: 100%;
box-sizing: initial;
background-color: #fff;
}
.markdown-body img[align=right] {
padding-left: 20px;
}
.markdown-body img[align=left] {
padding-right: 20px;
}
.markdown-body code {
padding: .2em .4em;
margin: 0;
font-size: 85%;
background-color: rgba(27, 31, 35, .05);
border-radius: 3px;
}
.markdown-body pre {
word-wrap: normal;
}
.markdown-body pre>code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
}
.markdown-body pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: initial;
border: 0;
}
.markdown-body .commit-tease-sha {
display: inline-block;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 90%;
color: #444d56;
}
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
color: #005cc5;
border-color: #005cc5;
}
.markdown-body .blob-wrapper {
overflow-x: auto;
overflow-y: hidden;
}
.markdown-body .blob-wrapper-embedded {
max-height: 240px;
overflow-y: auto;
}
.markdown-body .blob-num {
width: 1%;
min-width: 50px;
padding-right: 10px;
padding-left: 10px;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 12px;
line-height: 20px;
color: rgba(27, 31, 35, .3);
text-align: right;
white-space: nowrap;
vertical-align: top;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.markdown-body .blob-num:hover {
color: rgba(27, 31, 35, .6);
}
.markdown-body .blob-num:before {
content: attr(data-line-number);
}
.markdown-body .blob-code {
position: relative;
padding-right: 10px;
padding-left: 10px;
line-height: 20px;
vertical-align: top;
}
.markdown-body .blob-code-inner {
overflow: visible;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 12px;
color: #24292e;
word-wrap: normal;
white-space: pre;
}
.markdown-body .pl-token.active,
.markdown-body .pl-token:hover {
cursor: pointer;
background: #ffea7f;
}
.markdown-body .tab-size[data-tab-size="1"] {
-moz-tab-size: 1;
tab-size: 1;
}
.markdown-body .tab-size[data-tab-size="2"] {
-moz-tab-size: 2;
tab-size: 2;
}
.markdown-body .tab-size[data-tab-size="3"] {
-moz-tab-size: 3;
tab-size: 3;
}
.markdown-body .tab-size[data-tab-size="4"] {
-moz-tab-size: 4;
tab-size: 4;
}
.markdown-body .tab-size[data-tab-size="5"] {
-moz-tab-size: 5;
tab-size: 5;
}
.markdown-body .tab-size[data-tab-size="6"] {
-moz-tab-size: 6;
tab-size: 6;
}
.markdown-body .tab-size[data-tab-size="7"] {
-moz-tab-size: 7;
tab-size: 7;
}
.markdown-body .tab-size[data-tab-size="8"] {
-moz-tab-size: 8;
tab-size: 8;
}
.markdown-body .tab-size[data-tab-size="9"] {
-moz-tab-size: 9;
tab-size: 9;
}
.markdown-body .tab-size[data-tab-size="10"] {
-moz-tab-size: 10;
tab-size: 10;
}
.markdown-body .tab-size[data-tab-size="11"] {
-moz-tab-size: 11;
tab-size: 11;
}
.markdown-body .tab-size[data-tab-size="12"] {
-moz-tab-size: 12;
tab-size: 12;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item input {
margin: 0 .2em .25em -1.6em;
vertical-align: middle;
}

View File

@ -0,0 +1,281 @@
.aside-box {
position: relative;
background-color: white;
border-right: 1px solid rgb(245, 245, 245);
overflow: hidden;
padding: 0;
.header {
display: flex;
flex-direction: row;
align-items: center;
padding: 0 15px;
.from {
flex: 1 1;
flex-shrink: 0;
height: 40px;
/deep/.el-input .el-input__inner {
border-radius: 20px;
width: 170px;
}
}
.tools {
flex-basis: 32px;
flex-shrink: 0;
height: 32px;
margin-bottom: 8px;
cursor: pointer;
line-height: 32px;
text-align: center;
position: relative;
user-select: none;
.tools-menu {
position: absolute;
right: 0;
top: 38px;
width: 100px;
min-height: 80px;
box-sizing: border-box;
background-color: rgba(31, 35, 41, 0.9);
border-radius: 5px;
z-index: 1;
padding: 3px 0;
.menu1-item {
height: 40px;
line-height: 40px;
color: white;
font-size: 14px;
&:hover {
background-color: rgba(70, 72, 73, 0.9);
}
}
}
}
}
}
// 右侧面板
.panel {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
.header {
display: flex;
align-items: center;
justify-content: space-between;
&.border{
border-bottom: 1px solid #f5f5f5;
}
}
.subheader {
display: flex;
align-items: center;
justify-content: flex-start;
border-top: 1px solid rgb(92, 156, 230);
border-bottom: 1px solid rgb(92, 156, 230);
p {
padding: 0 10px;
cursor: pointer;
font-size: 13px;
&:first-child {
padding-left: 0;
}
&.active {
color: #508afe;
}
}
}
.panel-body {
overflow: auto;
width: 100%;
height: 100%;
box-sizing: border-box;
.preloading {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
user-select: none;
p {
margin-top: 20px;
color: #afacac;
font-size: 14px;
font-weight: 300;
}
}
.data-item {
display: flex;
flex-direction: row;
align-items: center;
height: 60px;
cursor: pointer;
padding: 5px 15px;
position: relative;
overflow: hidden;
border-bottom: 1px solid #f1ebeb;
margin-bottom: 2px;
.avatar {
height: 35px;
width: 35px;
flex-basis: 35px;
flex-shrink: 0;
background-color: #508afe;
border-radius: 50%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: white;
user-select: none;
transition: ease 1s;
position: relative;
}
.card {
height: 40px;
display: flex;
align-content: center;
flex-direction: column;
flex: 1 1;
margin-left: 10px;
overflow: hidden;
.title {
width: 100%;
height: 20px;
display: flex;
align-items: center;
.name {
margin-right: 15px;
color: #1f2329;
}
.larkc-tag {
font-size: 12px;
font-weight: 400;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 6px;
height: 20px;
border-radius: 2px;
cursor: default;
user-select: none;
background-color: #dee0e3;
transform: scale(0.8);
transform-origin: left;
flex-shrink: 0;
}
.wait {
background: #ffb445;
color: white;
}
.agree {
background: #53bd53;
color: white;
}
}
.content {
font-size: 10px;
line-height: 18px;
color: #8f959e;
overflow: hidden;
margin-top: 3px;
font-weight: 300;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.apply-from {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: relative;
right: -110px;
top: 0px;
height: 60px;
width: 100px;
transition: ease 0.5s 0.3s;
background-color: white;
opacity: 0;
button {
margin: 2px;
}
}
&:hover {
box-shadow: 0 0 8px 4px #f1f1f1;
.avatar {
border-radius: 2px;
}
.apply-from {
opacity: 1;
right: 0px;
}
}
}
}
}
.broadside-box {
position: absolute;
width: 350px;
height: 100%;
top: 0;
right: 0;
z-index: 2;
animation: showBox 0.5s ease-in-out;
-webkit-animation: showBox 0.5s ease-in-out;
-moz-animation: showBox 0.5s ease-in-out;
-webkit-box-direction: normal;
background: white;
box-shadow: 0 0 14px #cccccc70;
}
@keyframes showBox {
0% {
transform: translateX(350px);
}
to {
transform: translateX(0);
}
}
@-webkit-keyframes showBox {
0% {
-webkit-transform: translateX(350px);
}
to {
-webkit-transform: translateX(0);
}
}

View File

@ -0,0 +1,198 @@
/deep/.el-input__inner {
border-radius: 1px !important;
}
#auth-container {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: #f6f8fb;
#logo-name {
width: 200px;
height: 38px;
font-size: 34px;
font-family: Times New Roman, Georgia, Serif;
color: #2196f3;
margin-left: 20px;
margin-top: 20px;
}
#login-box {
position: absolute;
width: 350px;
min-height: 480px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 5px;
box-shadow: 0 0 0 #ccc;
box-shadow: 0 4px 14px 0 rgba(206, 207, 209, 0.5);
padding: 10px 20px;
.header {
width: 100%;
height: 38px;
font-size: 22px;
margin: 25px 0 20px 0;
}
.main {
width: 100%;
.links {
display: flex;
justify-content: space-between;
align-items: center;
a {
font-weight: normal !important;
}
}
.send-code-btn {
width: 140px;
height: 40px;
line-height: 40px;
display: inline-block;
background: #f3ecec;
text-align: center;
color: #777373;
cursor: pointer;
user-select: none;
margin-left: 5px;
&:active {
background: #e4dbdb;
}
}
.send-sms-disable {
cursor: not-allowed !important;
background: #f7f7f7 !important;
color: silver !important;
}
.submit-btn {
width: 100%;
border-radius: 2px;
}
}
}
}
.preview-account {
text-align: center;
p {
height: 25px;
line-height: 25px;
color: rgb(45, 44, 44);
font-weight: 100;
font-size: 12px;
}
}
.copyright {
position: absolute;
bottom: 30px;
left: 0;
right: 0;
width: 70%;
text-align: center;
margin: 0 auto;
font-size: 12px;
color: #b1a0a0;
a {
color: #777272;
font-weight: 400;
}
}
@media screen and (max-height: 500px) {
.copyright {
display: none;
}
}
.fly-box {
.fly {
pointer-events: none;
position: fixed;
z-index: 100;
}
.bg-fly-circle1 {
left: 40px;
top: 100px;
width: 100px;
height: 100px;
border-radius: 50%;
background: linear-gradient(
to right,
rgba(100, 84, 239, 0.07) 0%,
rgba(48, 33, 236, 0.04) 100%
);
animation: move 2.5s linear infinite;
}
.bg-fly-circle2 {
left: 3%;
top: 60%;
width: 150px;
height: 150px;
border-radius: 50%;
background: linear-gradient(
to right,
rgba(100, 84, 239, 0.08) 0%,
rgba(48, 33, 236, 0.04) 100%
);
animation: move 3s linear infinite;
}
.bg-fly-circle3 {
right: 2%;
top: 140px;
width: 145px;
height: 145px;
border-radius: 50%;
background: linear-gradient(
to right,
rgba(100, 84, 239, 0.1) 0%,
rgba(48, 33, 236, 0.04) 100%
);
animation: move 2.5s linear infinite;
}
.bg-fly-circle4 {
right: 5%;
top: 60%;
width: 160px;
height: 160px;
border-radius: 50%;
background: linear-gradient(
to right,
rgba(100, 84, 239, 0.02) 0%,
rgba(48, 33, 236, 0.04) 100%
);
animation: move 3.5s linear infinite;
}
}
@keyframes move {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(25px);
}
100% {
transform: translateY(0px);
}
}

View File

@ -0,0 +1,62 @@
* {
margin: 0;
padding: 0;
}
body,
html {
height: 100%;
min-width: 500px;
font-family: "Microsoft YaHei";
font-size: 16px;
color: #333;
}
button,
input,
select,
textarea {
font-size: 100%;
margin: 0;
padding: 0;
border: none;
outline: none;
}
img {
border: 0;
}
a,
img {
-webkit-touch-callout: none
}
a {
text-decoration: none;
}
textarea {
resize: none;
outline: 0;
white-space: pre-wrap;
word-wrap: break-word;
border: none;
background: #fff;
font-family: "Microsoft YaHei";
}
:focus {
outline: none;
}
.clearfix {
clear: both;
content: "";
display: block;
overflow: hidden
}
.clear {
clear: both;
}

View File

@ -0,0 +1,50 @@
.message-group {
min-height: 30px;
display: flex;
margin-bottom: 5px;
flex-direction: row;
padding: 3px 12px 3px 0;
&:first-child {
margin-top: 10px;
}
.left-box {
width: 50px;
flex-shrink: 0;
display: flex;
justify-content: center;
user-select: none;
padding-top: 8px;
img {
height: 30px;
width: 30px;
border-radius: 3px;
cursor: pointer;
}
}
.right-box {
flex: auto;
overflow-x: auto;
padding: 0px 5px 15px 5px;
.msg-header {
height: 30px;
line-height: 30px;
font-size: 12px;
color: #a09a9a;
position: relative;
user-select: none;
.name {
color: #333;
}
}
/deep/.text-message {
border-radius: 0;
}
}
}

View File

@ -0,0 +1,10 @@
//主题皮肤 - 预留功能
:root {
--themeBagColor: red;
}
// ------- 定义 Less 变量 -------
@themeBagColor: var(--themeBagColor);
// 遮罩层背景颜色
@maskBagColor: rgba(31, 35, 41, .3);

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

View File

@ -0,0 +1,313 @@
<template>
<div class="lum-dialog-mask">
<div class="container animated bounceInDown" :class="{ 'full-screen': isFullScreen }">
<el-container class="full-height">
<el-header class="header no-padding" height="50px">
<div class="tools">
<span>选择编程语言:&nbsp;&nbsp;</span>
<el-select v-model="language" size="mini" filterable placeholder="语言类型" :disabled="!editMode">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<i class="el-icon-close close-btn" @click="close" />
<i class="iconfont icon-full-screen" :class="{
'icon-tuichuquanping': isFullScreen,
'icon-quanping ': !isFullScreen,
}" :title="isFullScreen ? '关闭全屏模式' : '打开全屏模式'" @click="isFullScreen = !isFullScreen" />
</el-header>
<el-main class="main no-padding">
<PrismEditor class="peditor" style="border-radius: 0" :code="code" :language="language" :line-numbers="true"
@change="codeChanged" />
</el-main>
<el-footer class="footer no-padding" height="50px">
<div class="code-num">
<span>代码字数{{ code.length }}</span>
<span v-show="code.length > 10000 && editMode" class="code-warning">
(字数不能超过10000字)
</span>
</div>
<div class="buttom-group">
<el-button size="small" plain @click="close">
{{ editMode ? '取消编辑' : '关闭预览' }}
</el-button>
<el-button v-show="editMode" type="primary" size="small" @click="submit">
</el-button>
</div>
</el-footer>
</el-container>
</div>
</div>
</template>
<script>
import PrismEditor from "vue-prism-editor";
import "vue-prism-editor/dist/VuePrismEditor.css";
import "prismjs/themes/prism-okaidia.css";
import Vue from "vue";
import { Select, Option } from "element-ui";
Vue.use(Select);
Vue.use(Option);
export default {
name: "TalkCodeBlock",
components: {
PrismEditor,
},
props: {
loadCode: {
type: String,
default: "",
},
loadLang: {
type: String,
default: "",
},
editMode: {
type: Boolean,
default: false,
},
},
data() {
return {
language: "",
code: "",
options: [
{
value: "css",
label: "css",
},
{
value: "less",
label: "less",
},
{
value: "javascript",
label: "javascript",
},
{
value: "json",
label: "json",
},
{
value: "bash",
label: "bash",
},
{
value: "c",
label: "c",
},
{
value: "cil",
label: "cil",
},
{
value: "docker",
label: "docker",
},
{
value: "git",
label: "git",
},
{
value: "go",
label: "go",
},
{
value: "java",
label: "java",
},
{
value: "lua",
label: "lua",
},
{
value: "nginx",
label: "nginx",
},
{
value: "objectivec",
label: "objectivec",
},
{
value: "php",
label: "php",
},
{
value: "python",
label: "python",
},
{
value: "ruby",
label: "ruby",
},
{
value: "rust",
label: "rust",
},
{
value: "sql",
label: "sql",
},
{
value: "swift",
label: "swift",
},
{
value: "vim",
label: "vim",
},
{
value: "visual-basic",
label: "visual-basic",
},
{
value: "shell",
label: "shell",
},
],
isFullScreen: false,
};
},
watch: {
loadCode(value) {
this.code = value;
},
loadLang(value) {
this.language = value;
},
},
created() {
this.code = this.loadCode;
this.language = this.loadLang;
},
methods: {
submit() {
if (!this.code) {
this.$message.error("代码块不能为空...");
return false;
}
if (this.language == "") {
this.$message.error("请选择语言");
return false;
}
if (this.code.length > 10000) {
this.$message.error("代码字数不能超过10000字");
return false;
}
this.$emit("confirm", {
language: this.language,
code: this.code,
});
},
close() {
this.$emit("close");
},
codeChanged(code) {
this.code = code;
},
},
};
</script>
<style lang="less" scoped>
.container {
width: 80%;
max-width: 800px;
height: 600px;
overflow: hidden;
box-shadow: 0 2px 8px 0 rgba(31, 35, 41, 0.2);
transition: 0.5s ease;
background: #2d2d2d;
.header {
position: relative;
background-color: white;
.close-btn {
position: absolute;
right: 12px;
top: 13px;
font-size: 24px;
cursor: pointer;
}
.icon-full-screen {
position: absolute;
right: 45px;
top: 13px;
font-size: 20px;
cursor: pointer;
}
.tools {
line-height: 50px;
padding-left: 10px;
}
}
.footer {
background-color: #3c3c3c;
padding-right: 20px;
line-height: 50px;
.code-num {
float: left;
color: white;
padding-left: 10px;
font-size: 14px;
}
.code-warning {
color: red;
}
.buttom-group {
float: right;
height: 100%;
line-height: 50px;
text-align: right;
button {
border-radius: 0;
}
}
}
}
.full-screen {
width: 100%;
height: 100%;
max-width: 100%;
}
/deep/ .el-input__inner {
border-radius: 0;
width: 130px;
}
/deep/ pre {
border-radius: 0;
}
/deep/ .prism-editor-wrapper pre::-webkit-scrollbar {
background-color: #272822;
}
/deep/ .prism-editor-wrapper pre::-webkit-scrollbar-thumb {
background-color: #41413f;
cursor: pointer;
}
/deep/ .prism-editor-wrapper::-webkit-scrollbar {
background-color: #272822;
}
/deep/ .prism-editor-wrapper::-webkit-scrollbar-thumb {
background-color: rgb(114, 112, 112);
cursor: pointer;
}
</style>

View File

@ -0,0 +1,139 @@
<template>
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="no-padding header" height="60px">
<p>会话记录 ({{ records.length }})</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="no-padding main" v-loading="loading">
<el-scrollbar class="full-height" tag="section" :native="false">
<div v-for="record in records" :key="record.id" class="message-group">
<div class="left-box">
<el-avatar
fit="contain"
shape="square"
:size="30"
:src="record.avatar"
/>
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">
{{
record.nickname_remarks
? record.nickname_remarks
: record.nickname
}}
</span>
<el-divider direction="vertical" />
<span class="time">{{ record.created_at }}</span>
</div>
<!-- 文本消息 -->
<text-message
v-if="record.msg_type == 1"
:content="record.content"
/>
<!-- 文件 - 图片消息 -->
<image-message
v-else-if="record.msg_type == 2 && record.file.file_type == 1"
:src="record.file.file_url"
/>
<!-- 文件 - 音频消息 -->
<audio-message
v-else-if="record.msg_type == 2 && record.file.file_type == 2"
:src="record.file.file_url"
/>
<!-- 文件 - 视频消息 -->
<video-message
v-else-if="record.msg_type == 2 && record.file.file_type == 3"
/>
<!-- 文件 - 其它格式文件 -->
<file-message
v-else-if="record.msg_type == 2 && record.file.file_type == 4"
:file="record.file"
:record_id="record.id"
/>
<!-- 代码块消息 -->
<code-message
v-else-if="record.msg_type == 4"
:code="record.code_block.code"
:lang="record.code_block.code_lang"
/>
<div v-else class="other-message">未知消息类型</div>
</div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</div>
</template>
<script>
import { ServeGetForwardRecords } from '@/api/chat'
export default {
name: 'TalkForwardRecord',
data() {
return {
record_id: 0,
records: [],
loading: false,
isShow: false,
}
},
methods: {
open(record_id) {
if (record_id !== this.record_id) {
this.record_id = record_id
this.records = []
this.loadRecords()
}
this.isShow = true
},
close() {
this.isShow = false
},
loadRecords() {
this.loading = true
ServeGetForwardRecords({
record_id: this.record_id,
})
.then(res => {
if (res.code == 200) {
this.records = res.data.rows
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-mask {
z-index: 99999;
}
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 600px;
}
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
@import '~@/assets/css/talk/talk-records.less';
</style>

View File

@ -0,0 +1,602 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box" :class="{ 'full-screen': fullscreen }">
<el-header height="60px" class="header">
<p>消息管理器</p>
<p class="title">
<span>{{ query.talk_type == 1 ? "好友" : "群" }}{{ title }}</span>
</p>
<p class="tools">
<i
class="iconfont"
style="transform: scale(0.85)"
:class="fullscreen ? 'icon-tuichuquanping' : 'icon-quanping'"
@click="fullscreen = !fullscreen"
/>
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-header height="38px" class="sub-header">
<i
class="iconfont pointer"
:class="{ 'icon-shouqi2': broadside, 'icon-zhankai': !broadside }"
@click="triggerBroadside"
/>
<div class="search-box no-select">
<i class="el-icon-search" />
<input
v-model="search.keyword"
type="text"
maxlength="30"
placeholder="关键字搜索"
@keyup.enter="searchText($event)"
/>
</div>
</el-header>
<el-container class="full-height ov-hidden">
<el-aside width="200px" class="broadside" v-show="broadside">
<el-container class="full-height">
<el-header height="40px" class="aside-header">
<div
class="item"
:class="{ selected: contacts.show == 'friends' }"
@click="contacts.show = 'friends'"
>
我的好友({{ contacts.friends.length }})
</div>
<div class="item-shuxian">|</div>
<div
class="item"
:class="{ selected: contacts.show == 'groups' }"
@click="contacts.show = 'groups'"
>
我的群组({{ contacts.groups.length }})
</div>
</el-header>
<el-main class="no-padding">
<el-scrollbar class="full-height" tag="section" :native="false">
<div
v-for="item in contacts[contacts.show]"
class="contacts-item pointer"
:class="{
selected:
query.talk_type == item.type &&
query.receiver_id == item.id,
}"
:key="item.id"
@click="triggerMenuItem(item)"
>
<div class="avatar">
<el-avatar :size="20" :src="item.avatar">
<img src="~@/assets/image/detault-avatar.jpg" />
</el-avatar>
</div>
<div class="content" v-text="item.name"></div>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-aside>
<!-- 聊天记录阅览 -->
<el-main v-show="showBox == 0" class="no-padding">
<el-container class="full-height">
<el-header height="40px" class="type-items">
<span
v-for="tab in tabType"
:class="{ active: query.msg_type == tab.type }"
@click="triggerLoadType(tab.type)"
>{{ tab.name }}
</span>
</el-header>
<el-main
v-if="records.isEmpty"
class="history-record animated fadeIn"
>
<div class="empty-records">
<img src="~@/assets/image/chat-search-no-message.png" />
<p>暂无聊天记录</p>
</div>
</el-main>
<el-main v-else class="history-record">
<el-scrollbar class="full-height" tag="section" :native="false">
<div
v-for="record in records.items"
:key="record.id"
class="message-group"
>
<div class="left-box">
<el-avatar
shape="square"
fit="contain"
:size="30"
:src="record.avatar"
/>
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">
{{
record.nickname_remarks
? record.nickname_remarks
: record.nickname
}}
</span>
<el-divider direction="vertical" />
<span class="time">{{ record.created_at }}</span>
</div>
<!-- 文本消息 -->
<text-message
v-if="record.msg_type == 1"
:content="record.content"
/>
<!-- 文件 - 图片消息 -->
<image-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 1
"
:src="record.file.file_url"
/>
<!-- 文件 - 音频消息 -->
<audio-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 2
"
:src="record.file.file_url"
/>
<!-- 文件 - 视频消息 -->
<video-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 3
"
/>
<!-- 文件 - 其它格式文件 -->
<file-message
v-else-if="
record.msg_type == 2 && record.file.file_type == 4
"
:file="record.file"
:record_id="record.id"
/>
<!-- 会话记录消息 -->
<forward-message
v-else-if="record.msg_type == 3"
:forward="record.forward"
:record_id="record.id"
/>
<!-- 代码块消息 -->
<code-message
v-else-if="record.msg_type == 4"
:code="record.code_block.code"
:lang="record.code_block.code_lang"
/>
<!-- 投票消息 -->
<vote-message
v-else-if="record.msg_type == 5"
:record_id="record.id"
:vote="record.vote"
/>
<div v-else class="other-message">未知消息类型</div>
</div>
</div>
<!-- 数据加载栏 -->
<div v-show="records.loadStatus == 1" class="load-button blue">
<i class="el-icon-loading" />
<span>加载数据中...</span>
</div>
<div v-show="records.loadStatus == 0" class="load-button">
<i class="el-icon-arrow-down" />
<span @click="loadChatRecord">...</span>
</div>
</el-scrollbar>
</el-main>
</el-container>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import { ServeGetContacts } from "@/api/contacts";
import { ServeFindTalkRecords } from "@/api/chat";
import { formatSize as renderSize, download, imgZoom } from "@/utils/functions";
export default {
name: "TalkSearchRecord",
props: {
params: {
type: Object,
default: () => {
return {
talk_type: 0,
receiver_id: 0,
title: "",
};
},
},
},
data() {
return {
fullscreen: false,
user_id: this.$store.state.user.id,
title: "",
//
broadside: false,
contacts: {
show: "friends",
friends: [],
groups: [],
},
query: {
talk_type: 0,
receiver_id: 0,
msg_type: 0,
},
//
records: {
record_id: 0,
items: [],
isEmpty: false,
loadStatus: 0,
},
showBox: 0,
tabType: [
{ name: "全部", type: 0 },
{ name: "文件", type: 2 },
{ name: "会话记录", type: 3 },
{ name: "代码块", type: 4 },
{ name: "群投票", type: 5 },
],
search: {
keyword: "", //
date: "", //
page: 1, //
totalPage: 50, //
items: [], //
isShowDate: false,
},
};
},
mounted() {
this.title = this.params.title;
this.query = {
talk_type: this.params.talk_type,
receiver_id: this.params.receiver_id,
msg_type: 0,
};
this.loadChatRecord(0);
},
created() {
this.loadFriends();
},
methods: {
download,
renderSize,
//
getImgStyle(url) {
return imgZoom(url, 200);
},
//
getForwardTitle(item) {
let arr = [...new Set(item.map((v) => v.nickname))];
return arr.join("、") + "的会话记录";
},
//
loadFriends() {
ServeGetContacts().then(({ code, data }) => {
if (code == 200) {
this.contacts.friends = data.map((item) => {
return {
id: item.id,
type: 1,
avatar: item.avatar,
name: item.friend_remark ? item.friend_remark : item.nickname,
};
});
}
});
},
//
triggerMenuItem(item) {
this.title = item.name;
this.query.talk_type = item.type;
this.query.receiver_id = item.id;
this.showBox = 0;
this.triggerLoadType(0);
},
//
loadChatRecord() {
let data = {
talk_type: this.query.talk_type,
receiver_id: this.query.receiver_id,
record_id: this.records.record_id,
msg_type: this.query.msg_type,
};
if (this.records.loadStatus == 1) return;
this.records.loadStatus = 1;
ServeFindTalkRecords(data)
.then((res) => {
if (res.code != 200) return;
let records = data.record_id == 0 ? [] : this.records.items;
records.push(...res.data.rows);
this.records.items = records;
this.records.loadStatus =
res.data.rows.length < res.data.limit ? 2 : 0;
if (this.records.items.length == 0) {
this.records.isEmpty = true;
} else {
this.records.record_id =
this.records.items[this.records.items.length - 1].id;
}
})
.catch(() => {
this.records.loadStatus = 0;
});
},
triggerLoadType(type) {
this.records.record_id = 0;
this.query.msg_type = type;
this.records.isEmpty = false;
this.records.items = [];
this.loadChatRecord();
},
searchText() {
if (this.search.keyword == "") {
this.showBox = 0;
return false;
}
this.$notify.info({
title: "消息",
message: "查询功能正在开发中...",
});
},
triggerBroadside() {
this.broadside = !this.broadside;
},
},
};
</script>
<style lang="less" scoped>
/deep/.el-scrollbar__wrap {
overflow-x: hidden;
}
.lum-dialog-mask {
z-index: 1;
}
.lum-dialog-box {
width: 100%;
height: 600px;
max-width: 800px;
transition: 1s ease;
&.full-screen {
width: 100%;
height: 100%;
max-width: unset;
margin: 0;
border-radius: 0px;
}
.sub-header {
height: 38px;
line-height: 38px;
font-size: 12px;
border-bottom: 1px solid #f9f4f4;
margin-top: 10px;
padding: 0 10px;
position: relative;
i {
font-size: 22px;
color: #6f6a6a;
}
.search-box {
position: absolute;
width: 230px;
height: 32px;
top: 2px;
right: 10px;
background: #f9f4f4;
border-radius: 5px;
i {
position: absolute;
left: 10px;
top: 8px;
font-size: 16px;
}
input {
position: absolute;
left: 35px;
top: 3px;
height: 25px;
width: 184px;
color: #7d7171;
background: #f9f4f4;
}
}
}
.broadside {
@border: 1px solid #f9f9f9;
border-right: @border;
user-select: none;
transition: 3s ease;
.aside-header {
display: flex;
flex-direction: row;
height: 100%;
border-bottom: @border;
padding: 0;
> div {
text-align: center;
line-height: 40px;
font-size: 13px;
font-weight: 400;
}
.item {
flex: 1;
cursor: pointer;
&.selected {
color: #66b1ff;
}
}
.item-shuxian {
flex-basis: 1px;
flex-shrink: 0;
color: rgb(232 224 224);
}
}
.contacts-item {
height: 35px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-left: 10px;
position: relative;
.avatar {
flex-basis: 40px;
flex-shrink: 0;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.content {
flex: 1 1;
height: 100%;
line-height: 35px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
padding-right: 10px;
}
&:hover,
&.selected {
background-color: #f5f5f5;
}
}
}
}
/* first box */
.type-items {
padding: 0 0 0 10px;
line-height: 40px;
user-select: none;
border-bottom: 1px solid #f9f4f4;
.active {
color: #03a9f4;
font-weight: 500;
font-size: 13px;
}
span {
height: 40px;
width: 45px;
text-align: center;
cursor: pointer;
margin: 0 10px;
font-size: 12px;
font-weight: 400;
}
}
.history-record {
padding: 10px 0;
}
.load-button {
width: 100%;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: row;
&.blue {
color: #51b2ff;
}
span {
margin-left: 5px;
font-size: 13px;
cursor: pointer;
user-select: none;
}
}
.empty-records {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: #cccccc;
font-weight: 300;
font-size: 14px;
img {
width: 100px;
}
}
@import "~@/assets/css/talk/talk-records.less";
</style>

View File

@ -0,0 +1,193 @@
<template>
<div class="audio-message">
<div class="videodisc">
<div class="disc" :class="{ play: isPlay }" @click="toPlay">
<i v-if="loading" class="el-icon-loading" />
<i v-else-if="isPlay" class="el-icon-video-pause" />
<i v-else class="el-icon-video-play" />
<audio
ref="audio"
type="audio/mp3"
:src="src"
@timeupdate="timeupdate"
@ended="ended"
@canplay="canplay"
></audio>
</div>
</div>
<div class="detail">
<div class="text">
<i class="el-icon-service" />
<span>{{ getCurrDuration }} / {{ getTotalDuration }}</span>
</div>
<div class="process">
<el-progress :percentage="progress" :show-text="false" />
</div>
</div>
</div>
</template>
<script>
function formatSeconds(value) {
var theTime = parseInt(value) //
var theTime1 = 0 //
var theTime2 = 0 //
if (theTime > 60) {
theTime1 = parseInt(theTime / 60)
theTime = parseInt(theTime % 60)
if (theTime1 > 60) {
theTime2 = parseInt(theTime1 / 60)
theTime1 = parseInt(theTime1 % 60)
}
}
var result = '' + parseInt(theTime) //
if (10 > theTime > 0) {
result = '0' + parseInt(theTime) //
} else {
result = '' + parseInt(theTime) //
}
if (10 > theTime1 > 0) {
result = '0' + parseInt(theTime1) + ':' + result //0
} else {
result = '' + parseInt(theTime1) + ':' + result //
}
if (theTime2 > 0) {
result = '' + parseInt(theTime2) + ':' + result //
}
return result
}
export default {
name: 'AudioMessage',
props: {
src: {
type: String,
default: '',
},
},
data() {
return {
loading: true,
isPlay: false,
duration: 0,
currentTime: 0,
progress: 0,
}
},
computed: {
getTotalDuration() {
return formatSeconds(this.duration)
},
getCurrDuration() {
return formatSeconds(this.currentTime)
},
},
methods: {
toPlay() {
if (this.loading) {
return
}
let audio = this.$refs.audio
if (this.isPlay) {
audio.pause()
} else {
audio.play()
}
this.isPlay = !this.isPlay
},
//
timeupdate() {
let audio = this.$refs.audio
this.currentTime = audio.currentTime
this.progress = (audio.currentTime / audio.duration) * 100
},
// /
canplay() {
this.duration = this.$refs.audio.duration
this.loading = false
},
//
ended() {
this.isPlay = false
},
},
}
</script>
<style scoped lang="less">
.audio-message {
width: 200px;
height: 60px;
border-radius: 5px;
background: #ffffff;
display: flex;
align-items: center;
border: 1px solid #03a9f4;
overflow: hidden;
> div {
height: 100%;
}
.videodisc {
flex-basis: 60px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
.disc {
width: 42px;
height: 42px;
background: #e9e5e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
transition: ease 0.5;
&.play {
background: #ff5722;
box-shadow: 0 0 4px 0px #f76a3e;
}
i {
font-size: 24px;
}
&:active i {
transform: scale(1.1);
}
}
}
.detail {
flex: 1 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 10px;
.text {
width: 90%;
font-size: 12px;
i {
margin-right: 5px;
}
}
.process {
padding-top: 10px;
height: 20px;
width: 90%;
}
}
}
</style>

View File

@ -0,0 +1,148 @@
<template>
<div
class="code-message"
:class="{
'max-height': lineNumber > 6,
'max-width': maxwidth,
'full-screen': fullscreen,
}"
>
<i
:class="
fullscreen ? 'el-icon-close' : 'iconfont icon-tubiao_chakangongyi'
"
@click="fullscreen = !fullscreen"
/>
<pre class="lum-scrollbar" v-html="formatCode(code, lang)" />
</div>
</template>
<script>
import Prism from 'prismjs'
import 'prismjs/themes/prism-okaidia.css'
export default {
name: 'CodeMessage',
props: {
code: {
type: [String, Number],
default: '',
},
lang: {
type: String,
default: '',
},
maxwidth: {
type: Boolean,
default: false,
},
},
data() {
return {
fullscreen: false,
lineNumber: 0,
}
},
created() {
this.lineNumber = this.code.split(/\n/).length
},
methods: {
formatCode(code, lang) {
try {
return Prism.highlight(code, Prism.languages[lang], lang) + '<br/>'
} catch (error) {
return code
}
},
},
}
</script>
<style lang="less" scoped>
.code-message {
position: relative;
overflow: hidden;
border-radius: 5px;
box-sizing: border-box;
&.max-width {
max-width: 500px;
}
&.max-height {
height: 208px;
}
i {
position: absolute;
right: 0px;
top: 0px;
font-size: 16px;
cursor: pointer;
color: white;
display: inline-block;
opacity: 0;
width: 50px;
height: 30px;
background: #171616;
text-align: center;
line-height: 30px;
border-radius: 0 0 0px 8px;
transition: 1s ease;
}
&:hover {
i {
opacity: 1;
}
}
pre {
box-sizing: border-box;
height: 100%;
width: 100%;
overflow: auto;
padding: 10px;
line-height: 24px;
background: #272822;
color: #d5d4d4;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 85%;
&.lum-scrollbar {
&::-webkit-scrollbar {
background-color: black;
}
}
}
&.full-screen {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
max-width: unset;
max-height: unset;
border-radius: 0px;
background: #272822;
z-index: 99999999;
i {
position: fixed;
top: 15px;
right: 15px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 24px;
&:active {
box-shadow: 0 0 5px 0px #ccc;
}
}
}
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="file-message">
<div class="main">
<div class="ext">{{ ext }}</div>
<div class="file-box">
<p class="info">
<span class="name">{{ fileName }}</span>
<span class="size">({{ fileSize }})</span>
</p>
<p class="notice">文件已成功发送, 文件助手永久保存</p>
</div>
</div>
<div class="footer">
<a @click="download(record_id)"></a>
<a>在线预览</a>
</div>
</div>
</template>
<script>
import { formatSize, download } from '@/utils/functions'
export default {
name: 'FileMessage',
props: {
file: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
file_id: 0,
ext: '',
fileName: '',
fileSize: '',
}
},
created() {
this.file_id = this.file.id
this.ext = this.file.file_suffix.toUpperCase()
this.fileName = this.file.original_name
this.fileSize = formatSize(this.file.file_size)
},
methods: {
download,
},
}
</script>
<style lang="less" scoped>
.file-message {
width: 250px;
height: 85px;
background: white;
box-shadow: 0 0 5px 0px #e8e4e4;
padding: 10px;
border-radius: 3px;
transition: all 0.5s;
&:hover {
box-shadow: 0 0 5px 0px #cac6c6;
}
.main {
height: 45px;
display: flex;
flex-direction: row;
.ext {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 45px;
flex-shrink: 0;
color: #ffffff;
background: #49a4ff;
border-radius: 5px;
font-size: 12px;
}
.file-box {
flex: 1 1;
height: 45px;
margin-left: 10px;
overflow: hidden;
.info {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
height: 24px;
color: rgb(76, 76, 76);
font-size: 14px;
.name {
flex: 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.size {
font-size: 12px;
color: #cac6c6;
}
}
.notice {
height: 25px;
line-height: 25px;
font-size: 12px;
color: #929191;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.footer {
height: 30px;
line-height: 37px;
color: #409eff;
text-align: right;
font-size: 12px;
border-top: 1px solid #eff7ef;
margin-top: 10px;
a {
margin: 0 3px;
user-select: none;
cursor: pointer;
&:hover {
color: royalblue;
}
}
}
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<div>
<div class="forward-message" @click="catForwardRecords">
<div class="title">{{ title }}</div>
<div v-for="(record, index) in records" :key="index" class="lists">
<p>
<span>{{ record.nickname }}</span>
<span>{{ record.text }}</span>
</p>
</div>
<div class="footer">
<span>转发聊天会话记录 ({{ num }})</span>
</div>
</div>
<!-- 会话记录查看器 -->
<talk-forward-record ref="forwardRecordsRef" />
</div>
</template>
<script>
import TalkForwardRecord from '@/components/chat/TalkForwardRecord'
export default {
name: 'ForwardMessage',
components: {
TalkForwardRecord,
},
props: {
forward: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
default: 0,
},
},
data() {
return {
title: '',
records: [],
num: 0,
}
},
methods: {
catForwardRecords() {
this.$refs.forwardRecordsRef.open(this.record_id)
},
getForwardTitle(list) {
let arr = [...new Set(list.map(v => v.nickname))]
return arr.join('、') + '的会话记录'
},
},
created() {
let forward = this.forward
this.num = forward.num
this.records = forward.list
this.title = this.getForwardTitle(this.records)
},
}
</script>
<style lang="less" scoped>
/* 会话记录消息 */
.forward-message {
width: 250px;
min-height: 95px;
max-height: 150px;
border-radius: 3px;
background-color: white;
padding: 3px 10px;
cursor: pointer;
box-shadow: 0 0 5px 0px #e8e4e4;
text-align: left;
user-select: none;
.title {
height: 30px;
line-height: 30px;
font-size: 14px;
color: #565353;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
}
.lists p {
height: 18px;
line-height: 18px;
font-size: 10px;
color: #aaa9a9;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 100;
}
.footer {
height: 32px;
line-height: 35px;
color: #858282;
border-top: 1px solid #f1ebeb;
font-size: 12px;
margin-top: 12px;
font-weight: 300;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
box-shadow: 0 0 5px 0px #cac6c6;
}
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<!-- 好友申请消息 -->
<div class="apply-card">
<div class="card-header">
<img class="avatar" :src="avatar" />
<div class="nickname">No. {{ nickname }}</div>
<div class="datetime">{{ datetime }}</div>
<div class="remarks">
<span>备注信息{{ remarks }}</span>
</div>
</div>
<div class="card-footer">
<div class="mini-button" @click="handle(1)"></div>
<el-divider direction="vertical"></el-divider>
<div class="mini-button" @click="handle(2)"></div>
</div>
</div>
</template>
<script>
export default {
name: 'FriendApplyMessage',
props: {
data: {
type: Object,
default() {
return {}
},
},
},
data() {
return {
avatar:
'http://im-img.gzydong.club/media/images/avatar/20210602/60b6f03598ed0104301.png',
nickname: '独特态度',
datetime: '05/09 12:13 分',
remarks: '编辑个签,展示我的独特态度 展示我的独特态度。',
apply_id: 0,
}
},
created() {},
methods: {
handle(type) {
alert(type)
},
},
}
</script>
<style lang="less" scoped>
.apply-card {
position: relative;
width: 170px;
min-height: 180px;
border-radius: 15px;
overflow: hidden;
transition: all 0.5s;
box-sizing: border-box;
background-image: linear-gradient(-84deg, #1ab6ff 0, #1ab6ff 0, #82c1f3 100%);
// #028fff
&:hover {
transform: scale(1.02);
}
.card-header {
position: relative;
width: 100%;
height: 135px;
.avatar {
position: absolute;
top: 18px;
left: 8px;
width: 40px;
height: 40px;
border-radius: 50%;
background: white;
}
.nickname {
position: absolute;
top: 15px;
right: 8px;
width: 90px;
height: 25px;
font-size: 10px;
text-align: center;
line-height: 25px;
color: white;
border-bottom: 1px dashed white;
}
.datetime {
position: absolute;
top: 42px;
right: 11.5px;
color: white;
font-size: 10px;
transform: scale(0.9);
}
.remarks {
position: absolute;
bottom: 5px;
color: white;
font-size: 10px;
padding: 3px 5px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
transform: scale(0.95);
}
}
.card-footer {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
border-top: 1px solid white;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
/deep/.el-divider {
background: white;
}
.mini-button {
display: flex;
width: 50px;
height: 25px;
margin: 0 10px;
text-align: center;
align-items: center;
justify-content: center;
font-size: 13px;
color: white;
cursor: pointer;
&:hover {
font-size: 14px;
}
}
}
}
</style>

View File

@ -0,0 +1,51 @@
<template>
<div class="image-message no-select">
<el-image
fit="cover"
:src="src"
:lazy="true"
:style="getImgStyle(src)"
:preview-src-list="[src]"
>
<div slot="error" class="image-slot">图片加载失败...</div>
<div slot="placeholder" class="image-slot">图片加载中...</div>
</el-image>
</div>
</template>
<script>
import { imgZoom } from '@/utils/functions'
export default {
name: 'ImageMessage',
props: {
src: {
type: String,
default: '',
},
},
methods: {
getImgStyle(url) {
return imgZoom(url, 200)
},
},
}
</script>
<style lang="less" scoped>
.image-message {
/deep/.el-image {
border-radius: 5px;
cursor: pointer;
background: #f1efef;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
font-size: 13px;
color: #908686;
background: #efeaea;
}
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="invite-message">
<div v-if="invite.type == 1 || invite.type == 3" class="system-msg">
<a @click="toUser(invite.operate_user.id)">
{{ invite.operate_user.nickname }}
</a>
<span>{{ invite.type == 1 ? '邀请了' : '将' }}</span>
<template v-for="(user, uidx) in invite.users">
<a @click="toUser(user.id)">{{ user.nickname }}</a>
<em v-show="uidx < invite.users.length - 1"></em>
</template>
<span>{{ invite.type == 1 ? '加入了群聊' : '踢出了群聊' }}</span>
</div>
<div v-else-if="invite.type == 2" class="system-msg">
<a @click="toUser(invite.operate_user.id)">
{{ invite.operate_user.nickname }}
</a>
<span>退出了群聊</span>
</div>
</div>
</template>
<script>
export default {
name: 'InviteMessage',
props: {
invite: {
type: Object,
required: true,
},
},
methods: {
toUser(user_id) {
this.$emit('cat', user_id)
},
},
}
</script>
<style lang="less" scoped>
.invite-message {
display: flex;
justify-content: center;
}
.system-msg {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
span {
margin: 0 5px;
}
a {
color: #939596;
cursor: pointer;
font-size: 12px;
font-weight: 400;
&:hover {
color: black;
}
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<!-- 登录消息 -->
<div class="login-message">
<h4>登录操作通知</h4>
<p>登录时间{{ datetime }} (CST)</p>
<p>IP地址{{ ip }}</p>
<p>登录地点{{ address }}</p>
<p>登录设备{{ platform }}</p>
<p>异常原因{{ reason }}</p>
</div>
</template>
<script>
import { parseTime } from '@/utils/functions'
export default {
name: 'LoginMessage',
props: {
detail: {
type: Object,
required: true,
},
},
data() {
return {
datetime: '',
ip: '',
address: '',
platform: '',
reason: '常用设备登录',
}
},
created() {
this.ip = this.detail.ip
this.datetime = parseTime(
this.detail.created_at,
'{y}年{m}月{d}日 {h}:{i}:{s}'
)
this.address = this.detail.address
this.reason = this.detail.reason
this.platform =
this.getExploreName(this.detail.agent) +
' / ' +
this.getExploreOs(this.detail.agent)
},
methods: {
getExploreName(userAgent = '') {
if (userAgent.indexOf('Opera') > -1 || userAgent.indexOf('OPR') > -1) {
return 'Opera'
} else if (
userAgent.indexOf('compatible') > -1 &&
userAgent.indexOf('MSIE') > -1
) {
return 'IE'
} else if (userAgent.indexOf('Edge') > -1) {
return 'Edge'
} else if (userAgent.indexOf('Firefox') > -1) {
return 'Firefox'
} else if (
userAgent.indexOf('Safari') > -1 &&
userAgent.indexOf('Chrome') == -1
) {
return 'Safari'
} else if (
userAgent.indexOf('Chrome') > -1 &&
userAgent.indexOf('Safari') > -1
) {
return 'Chrome'
} else {
return 'Unkonwn'
}
},
getExploreOs(userAgent = '') {
if (userAgent.indexOf('Mac OS') > -1) {
return 'Mac OS'
} else {
return 'Windows'
}
},
},
}
</script>
<style lang="less" scoped>
.login-message {
width: 300px;
min-height: 50px;
background: #f7f7f7;
border-radius: 5px;
padding: 15px;
p {
font-size: 13px;
margin: 10px 0;
&:last-child {
margin-bottom: 0;
}
}
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<div class="reply-message">这是回复的消息[预留]</div>
</template>
<script>
export default {
name: 'ReplyMessage',
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.reply-message {
margin-top: 5px;
min-height: 28px;
background: #f7f1f1;
line-height: 28px;
font-size: 12px;
padding: 0 10px;
border-radius: 3px;
color: #a7a2a2;
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="revoke-message">
<div class="content">
<span v-if="$store.state.user.id == item.user_id">
你撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
<span v-else-if="item.talk_type == 1">
对方撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
<span v-else>
"{{ item.nickname }}" 撤回了一条消息 | {{ sendTime(item.created_at) }}
</span>
</div>
</div>
</template>
<script>
import { formatTime as sendTime } from "@/utils/functions";
export default {
name: "RevokeMessage",
props: {
item: {
type: Object,
},
},
methods: {
sendTime,
},
};
</script>
<style lang="less" scoped>
.revoke-message {
display: flex;
justify-content: center;
.content {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
span {
margin: 0 5px;
}
a {
color: #939596;
cursor: pointer;
font-size: 12px;
font-weight: 400;
&:hover {
color: black;
}
}
}
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<div class="system-text-message">
<div class="content">{{ content }}</div>
</div>
</template>
<script>
import { formatTime as sendTime } from '@/utils/functions'
export default {
name: 'SystemTextMessage',
props: {
content: String,
},
methods: {
sendTime,
},
}
</script>
<style lang="less" scoped>
.system-text-message {
display: flex;
justify-content: center;
.content {
margin: 10px auto;
background-color: #f5f5f5;
font-size: 11px;
line-height: 30px;
padding: 0 8px;
word-break: break-all;
word-wrap: break-word;
color: #979191;
user-select: none;
font-weight: 300;
display: inline-block;
border-radius: 3px;
}
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div
class="text-message"
:class="{
left: float == 'left',
right: float == 'right',
'max-width': !fullWidth,
}"
>
<div v-if="arrow" class="arrow"></div>
<pre v-html="html" />
</div>
</template>
<script>
import { textReplaceLink } from "@/utils/functions";
import { textReplaceEmoji } from "@/utils/emojis";
export default {
name: "TextMessage",
props: {
content: {
type: [String, Number],
default: "",
},
float: {
type: String,
default: "left",
},
fullWidth: {
type: Boolean,
default: true,
},
arrow: {
type: Boolean,
default: false,
},
},
data() {
return {
html: "",
};
},
created() {
const text = textReplaceLink(
this.content,
this.float == "right" ? "#ffffff" : "rgb(9 149 208)"
);
this.html = textReplaceEmoji(text);
},
};
</script>
<style lang="less" scoped>
@bg-left-color: #f5f5f5;
@bg-right-color: #1ebafc;
.text-message {
position: relative;
min-width: 30px;
min-height: 30px;
border-radius: 5px;
padding: 5px;
.arrow {
position: absolute;
width: 0;
height: 0;
font-size: 0;
border: 5px solid;
top: 6px;
left: -10px;
}
&.max-width {
max-width: calc(100% - 50px);
}
&.left {
color: #3a3a3a;
background: @bg-left-color;
.arrow {
border-color: transparent @bg-left-color transparent transparent;
}
}
&.right {
color: #fff;
background: @bg-right-color;
.arrow {
right: -10px;
left: unset;
border-color: transparent transparent transparent @bg-right-color;
}
}
pre {
white-space: pre-wrap;
overflow: hidden;
word-break: break-word;
word-wrap: break-word;
font-size: 15px;
padding: 3px 10px;
font-family: "Microsoft YaHei";
line-height: 25px;
}
}
</style>

View File

@ -0,0 +1,19 @@
<template>
<!-- 用户卡片消息 - 预留 -->
<div></div>
</template>
<script>
export default {
name: 'UserCardMessage',
components: {},
data() {
return {}
},
computed: {},
watch: {},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,18 @@
<template>
<div class="video-message">
视频消息
</div>
</template>
<script>
export default {
name: 'VideoMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,134 @@
<template>
<div class="visit-card-message">
<div class="user flex-center">
<div class="avatar flex-center">
<el-avatar :size="28" :src="avatar" />
</div>
<div class="content flex-center">
<p class="ellipsis">{{ nickname }}</p>
</div>
<div class="tools flex-center">
<span class="flex-center pointer">
<i class="el-icon-plus" /> 加好友
</span>
</div>
</div>
<div class="sign"><span>个性签名 : </span>{{ sign }}</div>
<div class="share no-select ellipsis">
<a class="pointer" @click="openVisitCard(friendId)">?</a>
分享了用户名片可点击添加好友 ...
</div>
</div>
</template>
<script>
export default {
name: 'VisitCardMessage',
data() {
return {
userId: 0,
friendId: 0,
avatar:
'http://im-serve0.gzydong.club/static/image/sys-head/2019012107542668696.jpg',
sign:
'这个社会,是赢家通吃,输者一无所有,社会,永远都是只以成败论英雄。',
nickname:
'氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳氨基酸纳',
}
},
created() {},
methods: {
openVisitCard(user_id) {
this.$emit('openVisitCard', user_id)
},
},
}
</script>
<style lang="less" scoped>
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.visit-card-message {
min-height: 130px;
min-width: 100px;
max-width: 300px;
border-radius: 5px;
padding: 10px;
box-sizing: border-box;
border: 1px solid #ece5e5;
transition: all 0.5s;
&:hover {
box-shadow: 0 0 8px #e2d3d3;
transform: scale(1.01);
}
.user {
height: 40px;
overflow: hidden;
box-sizing: border-box;
> div {
height: inherit;
}
.avatar {
flex-basis: 30px;
flex-shrink: 0;
}
.content {
flex: 1 1;
margin: 0 10px;
font-size: 14px;
justify-content: flex-start;
overflow: hidden;
}
.tools {
flex-basis: 60px;
flex-shrink: 0;
span {
width: 65px;
height: 30px;
background: #409eff;
color: white;
font-size: 13px;
border-radius: 20px;
padding: 0 8px;
transform: scale(0.8);
user-select: none;
&:active {
background: #83b0f3;
transform: scale(0.83);
}
}
}
}
.sign {
min-height: 22px;
line-height: 22px;
border-radius: 3px;
padding: 5px 8px;
background: #f3f5f7;
color: #7d7d7d;
font-size: 12px;
margin: 10px 0;
span {
font-weight: bold;
}
}
.share {
font-size: 12px;
color: #7d7d7d;
a {
color: #4cabf7;
}
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="voice-message"></div>
</template>
<script>
export default {
name: 'VoiceMessage',
components: {},
data() {
return {}
},
methods: {},
created() {},
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,299 @@
<template>
<div>
<div class="vote-message">
<div class="vote-from">
<div class="vheader">
<p>
{{ answer_mode == 1 ? "[多选投票]" : "[单选投票]" }}
<i
v-show="is_vote"
class="pointer"
:class="{
'el-icon-loading': refresh,
'el-icon-refresh': !refresh,
}"
title="刷新投票结果"
@click="loadRefresh"
></i>
</p>
<p>{{ title }}</p>
</div>
<template v-if="is_vote">
<div class="vbody">
<div class="vote-view" v-for="(option, index) in options">
<p class="vote-option">{{ option.value }}. {{ option.text }}</p>
<p class="vote-census">
{{ option.num }} {{ option.progress }}%
</p>
<p class="vote-progress">
<el-progress
:show-text="false"
:percentage="parseInt(option.progress)"
/>
</p>
</div>
</div>
<div class="vfooter vote-view">
<p>应参与人数{{ answer_num }} </p>
<p>实际参与人数{{ answered_num }} </p>
</div>
</template>
<template v-else>
<div class="vbody">
<p class="option" v-for="(option, index) in options">
<el-checkbox
v-model="option.is_checked"
@change="toSelect2(option)"
/>
<span @click="toSelect(option, index)" style="margin-left: 10px">
{{ option.value }} {{ option.text }}
</span>
</p>
</div>
<div class="vfooter">
<el-button plain round @click="toVote">
{{ isUserVote ? "立即投票" : "请选择进行投票" }}
</el-button>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
import { ServeConfirmVoteHandle } from "@/api/chat";
export default {
name: "VoteMessage",
props: {
vote: {
type: Object,
required: true,
},
record_id: {
type: Number,
required: true,
},
},
data() {
return {
answer_mode: 0,
title: "啊谁叫你打开你卡沙发那,那就是看、卡收纳是你",
radio_value: "",
options: [],
is_vote: false,
answer_num: 0,
answered_num: 0,
refresh: false,
};
},
computed: {
isUserVote() {
return this.options.some((iten) => {
return iten.is_checked;
});
},
},
created() {
let user_id = this.$store.state.user.id;
let { detail, statistics, vote_users } = this.vote;
this.answer_mode = detail.answer_mode;
this.answer_num = detail.answer_num;
this.answered_num = detail.answered_num;
detail.answer_option.forEach((item) => {
this.options.push({
value: item.key,
text: item.value,
is_checked: false,
num: 0,
progress: "00.0",
});
});
this.is_vote = vote_users.some((value) => {
return value == user_id;
});
this.updateStatistics(statistics);
},
methods: {
loadRefresh() {
this.refresh = true;
setTimeout(() => {
this.refresh = false;
}, 500);
},
updateStatistics(data) {
let count = data.count;
this.options.forEach((option) => {
option.num = data.options[option.value];
if (count > 0) {
option.progress = (data.options[option.value] / count) * 100;
}
});
},
toSelect(option, index) {
if (this.answer_mode == 0) {
this.options.forEach((option) => {
option.is_checked = false;
});
}
this.options[index].is_checked = !option.is_checked;
},
toSelect2(option) {
if (this.answer_mode == 0) {
this.options.forEach((item) => {
if (option.value == item.value) {
item.is_checked = option.is_checked;
} else {
item.is_checked = false;
}
});
}
},
toVote() {
if (this.isUserVote == false) {
return false;
}
let items = [];
this.options.forEach((item) => {
if (item.is_checked) {
items.push(item.value);
}
});
ServeConfirmVoteHandle({
record_id: this.record_id,
options: items.join(","),
}).then((res) => {
if (res.code == 200) {
this.is_vote = true;
this.updateStatistics(res.data);
}
});
},
},
};
</script>
<style lang="less" scoped>
.vote-message {
width: 300px;
min-height: 150px;
border: 1px solid #eceff1;
box-sizing: border-box;
border-radius: 5px;
overflow: hidden;
.vote-from {
width: 100%;
.vheader {
min-height: 50px;
background: #4e83fd;
padding: 8px;
position: relative;
p {
margin: 3px 0;
&:first-child {
color: rgb(245, 237, 237);
font-size: 13px;
margin-bottom: 8px;
}
&:last-child {
color: white;
}
}
&::before {
content: "投票";
position: absolute;
font-size: 60px;
color: white;
opacity: 0.1;
top: -5px;
right: 10px;
}
}
.vbody {
min-height: 80px;
width: 100%;
padding: 5px 15px;
box-sizing: border-box;
.option {
margin: 14px 0px;
font-size: 13px;
span {
cursor: pointer;
user-select: none;
line-height: 22px;
}
.el-radio {
margin-right: 0;
.el-radio__label {
padding-left: 5px;
}
}
}
margin-bottom: 10px;
}
.vfooter {
height: 55px;
text-align: center;
box-sizing: border-box;
.el-button {
width: 80%;
font-weight: 400;
}
&.vote-view {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
padding-left: 15px;
p {
border-left: 2px solid #2196f3;
padding-left: 5px;
}
}
}
}
.vote-view {
width: 100%;
min-height: 30px;
margin: 15px 0;
box-sizing: border-box;
> p {
margin: 6px 0px;
font-size: 13px;
}
.vote-option {
min-height: 20px;
line-height: 20px;
}
.vote-census {
height: 20px;
line-height: 20px;
}
}
}
</style>

View File

@ -0,0 +1,33 @@
import AudioMessage from './AudioMessage.vue';
import CodeMessage from './CodeMessage.vue';
import ForwardMessage from './ForwardMessage.vue';
import ImageMessage from './ImageMessage.vue';
import TextMessage from './TextMessage.vue';
import VideoMessage from './VideoMessage.vue';
import VoiceMessage from './VoiceMessage.vue';
import SystemTextMessage from './SystemTextMessage.vue';
import FileMessage from './FileMessage.vue';
import InviteMessage from './InviteMessage.vue';
import RevokeMessage from './RevokeMessage.vue';
import VisitCardMessage from './VisitCardMessage.vue';
import ReplyMessage from './ReplyMessage.vue';
import VoteMessage from './VoteMessage.vue';
import LoginMessage from './LoginMessage.vue';
export {
AudioMessage,
CodeMessage,
ForwardMessage,
ImageMessage,
TextMessage,
VideoMessage,
VoiceMessage,
SystemTextMessage,
FileMessage,
InviteMessage,
RevokeMessage,
VisitCardMessage,
ReplyMessage,
VoteMessage,
LoginMessage
}

View File

@ -0,0 +1,226 @@
<template>
<el-tabs v-model="activeName" @tab-click="handleClick" type="card" :stretch=true>
<el-tab-pane :label="toUser.storeFlag ? '正在咨询' : '他的足迹'" name="history">
<div style="margin-left: 12px;" v-if="toUser.storeFlag">
<GoodsLink :goodsDetail="goodsDetail" v-if="toUser.userId === goodsDetail.storeId"
@sendMessage="submitSendMessage" />
<FootPrint :list="footPrintList" @loadMore="loadMoreFootPrint()" :orderList="orderPrintList"
@sendMessage="submitSendMessage" />
</div>
</el-tab-pane>
<el-tab-pane label="店铺信息" name="UserInfo" v-if="toUser.storeFlag">
<div v-if="toUser.storeFlag">
<StoreDetail :storeInfo="storeInfo" />
</div>
</el-tab-pane>
</el-tabs>
</template>
<script>
import { Tabs, TabPane } from 'element-ui'
import { ServeGetStoreDetail, ServeGetUserDetail, ServeGetFootPrint, ServeGetOrderPrint } from '@/api/user'
import { ServeGetGoodsDetail } from '@/api/goods'
import StoreDetail from "@/components/chat/panel/template/storeDetail.vue";
import FootPrint from "@/components/chat/panel/template/footPrint.vue";
import GoodsLink from "@/components/chat/panel/template/goodsLink.vue";
import SocketInstance from "@/im-server/socket-instance";
import { mapState, mapGetters } from "vuex";
export default {
components: {
"el-tabs": Tabs,
"el-tab-pane": TabPane,
StoreDetail,
FootPrint,
GoodsLink
},
props: {
toUser: {
type: Object,
default: null,
},
id: {
type: String,
default: '',
},
goodsParams: {
type: Object,
default: null,
},
},
computed: {
...mapGetters(["talkItems"]),
...mapState({
index_name: (state) => state.dialogue.index_name,
}),
},
data () {
return {
activeName: 'history',
storeInfo: {}, //
memberInfo: {}, //
footPrintParams: {
pageSize: 10,
pageNumber: 1,
memberId: '',
storeId: '',
},
goodsDetail: {},
footPrintList: [], //
orderPrintList: []//
}
},
mounted () {
if (this.toUser.storeFlag) {
this.getStoreDetail()
} else {
this.getMemberDetail()
}
this.getFootPrint()
if (this.goodsParams) {
this.getGoodsDetail()
}
},
methods: {
getStoreDetail () {
ServeGetStoreDetail(this.toUser.userId).then(res => {
if (res.success) {
this.storeInfo = res.result
}
})
},
loadMoreFootPrint (e) {
//
this.footPrintParams.pageNumber++
this.getFootPrint()
},
handleClick () { },
getMemberDetail () {
ServeGetUserDetail(this.toUser.userId).then(res => {
if (res.success) {
this.memberInfo = res.result
}
})
},
getGoodsDetail () {
ServeGetGoodsDetail(this.goodsParams).then(res => {
if (res.success) {
this.goodsDetail = res.result.data
}
})
},
getFootPrint () {
if (this.toUser.storeFlag) {
this.footPrintParams.memberId = this.id
this.footPrintParams.storeId = this.toUser.userId
} else {
this.footPrintParams.memberId = this.toUser.userId
this.footPrintParams.storeId = this.id
}
ServeGetFootPrint(this.footPrintParams).then(res => {
res.result.records.forEach((item, index) => {
if (localStorage.getItem(item.goodsId)) {
item.btnHide = 0
} else {
item.btnHide = 1
}
if (item.goodsId === this.goodsParams.goodsId) {
res.result.records.splice(index, 1)
}
});
this.footPrintList.push(...res.result.records)
})
//
ServeGetOrderPrint(this.footPrintParams).then((res) => {
if (res.code == 200) {
res.result.records.forEach((item) => {
this.orderPrintList.push({
...item,
btnHide: 1
})
})
// this.orderPrintList.push(...res.result.records)
}
})
},
//
submitSendMessage (record, context, messageType) {
SocketInstance.emit("event_talk", record);
this.$store.commit("UPDATE_TALK_ITEM", {
index_name: this.index_name,
draft_text: "",
});
/**
* 插入数据
*/
const insterChat = {
createTime: this.formateDateAndTimeToString(new Date()),
fromUser: this.id,
toUser: record.to,
isRead: false,
messageType: messageType,
text: context,
float: "right",
};
//
this.$store.commit("PUSH_DIALOGUE", insterChat);
//
let el = document.getElementById("lumenChatPanel");
//
let isBottom =
Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight;
if (isBottom || record.to == this.id) {
this.$nextTick(() => {
el.scrollTop = el.scrollHeight;
});
} else {
this.$store.commit("SET_TLAK_UNREAD_MESSAGE", {
content: content,
nickname: record.name,
});
}
},
formateDateAndTimeToString (date) {
var hours = date.getHours();
var mins = date.getMinutes();
var secs = date.getSeconds();
var msecs = date.getMilliseconds();
if (hours < 10) hours = "0" + hours;
if (mins < 10) mins = "0" + mins;
if (secs < 10) secs = "0" + secs;
if (msecs < 10) secs = "0" + msecs;
return (
this.formatDateToString(date) + " " + hours + ":" + mins + ":" + secs
);
},
formatDateToString (date) {
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
if (month < 10) month = "0" + month;
if (day < 10) day = "0" + day;
return year + "-" + month + "-" + day;
},
}
}
</script>
<style scoped lang="less">
// /deep/ .el-tabs__nav.is-top {
// }
/deep/ .el-tabs__nav {
height: 60px;
line-height: 60px;
}
/deep/ .el-tab-pane {
margin-left: 12px;
}
</style>

View File

@ -0,0 +1,278 @@
<template>
<el-header id="panel-header">
<div class="module left-module">
<span
class="icon-badge"
v-show="params.is_robot == 0"
:class="{ 'red-color': params.talk_type == 1 }"
>
{{ params.talk_type == 1 ? '好友' : '群组' }}
</span>
<span class="nickname">{{ params.nickname }}</span>
<span v-show="params.talk_type == 2" class="num">({{ groupNum }})</span>
</div>
<div v-show="params.talk_type == 1 && params.is_robot == 0" class="module center-module">
<p class="online">
<span v-show="isOnline" class="online-status"></span>
<span>{{ isOnline ? '在线' : '离线' }}</span>
</p>
<p class="keyboard-status" v-show="isKeyboard"> ...</p>
</div>
<div class="module right-module" >
<el-tooltip content="历史消息" placement="top">
<p v-show="params.is_robot == 0">
<i class="el-icon-time" @click="triggerEvent('history')" />
</p>
</el-tooltip>
<el-tooltip content="群公告" placement="top">
<p v-show="params.talk_type == 2">
<i class="iconfont icon-gonggao2" @click="triggerEvent('notice')" />
</p>
</el-tooltip>
<el-tooltip content="群设置" placement="top">
<p v-show="params.talk_type == 2">
<i class="el-icon-setting" @click="triggerEvent('setting')" />
</p>
</el-tooltip>
</div>
</el-header>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: () => {
return {
talk_type: 0,
receiver_id: 0,
params: 0,
nickname: '',
}
},
},
online: {
type: Boolean,
default: false,
},
keyboard: {
type: [Boolean, Number],
default: false,
},
},
data() {
return {
params: {
talk_type: 0,
receiver_id: 0,
params: 0,
nickname: '',
},
isOnline: false,
isKeyboard: false,
groupNum: 0,
}
},
created() {
this.setParamsData(this.data)
this.setOnlineStatus(this.online)
},
watch: {
data(value) {
this.setParamsData(value)
},
online(value) {
this.setOnlineStatus(value)
},
keyboard(value) {
this.isKeyboard = value
setTimeout(() => {
this.isKeyboard = false
}, 2000)
},
},
methods: {
setOnlineStatus(value) {
this.isOnline = value
},
setParamsData(object) {
Object.assign(this.params, object)
},
setGroupNum(value) {
this.groupNum = value
},
triggerEvent(event_name) {
this.$emit('event', event_name)
},
},
}
</script>
<style lang="less" scoped>
#panel-header {
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
border-bottom: 1px solid #f5eeee;
.module {
width: 100%/3;
height: 100%;
display: flex;
align-items: center;
}
.left-module {
padding-right: 5px;
.icon-badge {
background: rgb(81 139 254);
height: 18px;
line-height: 18px;
padding: 1px 3px;
font-size: 10px;
color: white;
border-radius: 3px;
margin-right: 8px;
flex-shrink: 0;
&.red-color {
background: #f97348;
}
}
.nickname {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.center-module {
flex-direction: column;
justify-content: center;
.online {
color: #cccccc;
font-weight: 300;
font-size: 15px;
&.color {
color: #1890ff;
}
.online-status {
position: relative;
top: -1px;
display: inline-block;
width: 6px;
height: 6px;
vertical-align: middle;
border-radius: 50%;
position: relative;
background-color: #1890ff;
margin-right: 5px;
&:after {
position: absolute;
top: -1px;
left: -1px;
width: 100%;
height: 100%;
border: 1px solid #1890ff;
border-radius: 50%;
-webkit-animation: antStatusProcessing 1.2s ease-in-out infinite;
animation: antStatusProcessing 1.2s ease-in-out infinite;
content: '';
}
}
}
.keyboard-status {
height: 20px;
line-height: 18px;
font-size: 10px;
animation: inputfade 600ms infinite;
-webkit-animation: inputfade 600ms infinite;
}
}
.right-module {
display: flex;
justify-content: flex-end;
align-items: center;
p {
cursor: pointer;
margin: 0 8px;
font-size: 20px;
color: #828f95;
&:active i {
font-size: 26px;
transform: scale(1.3);
transition: ease 0.5s;
color: red;
}
}
}
}
/* css 动画 */
@keyframes inputfade {
from {
opacity: 1;
}
50% {
opacity: 0.4;
}
to {
opacity: 1;
}
}
@-webkit-keyframes inputfade {
from {
opacity: 1;
}
50% {
opacity: 0.4;
}
to {
opacity: 1;
}
}
@-webkit-keyframes antStatusProcessing {
0% {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
to {
-webkit-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
@keyframes antStatusProcessing {
0% {
-webkit-transform: scale(0.8);
transform: scale(0.8);
opacity: 0.5;
}
to {
-webkit-transform: scale(2.4);
transform: scale(2.4);
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,101 @@
<template>
<div class="multi-select">
<div class="multi-title">
<span>已选中{{ value }} 条消息</span>
</div>
<div class="multi-main">
<div class="btn-group">
<div
class="multi-icon pointer"
@click="$emit('event', 'merge_forward')"
>
<i class="el-icon-position" />
</div>
<p>合并转发</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'forward')">
<i class="el-icon-position" />
</div>
<p>逐条转发</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'delete')">
<i class="el-icon-delete" />
</div>
<p>批量删除</p>
</div>
<div class="btn-group">
<div class="multi-icon pointer" @click="$emit('event', 'close')">
<i class="el-icon-close" />
</div>
<p>关闭</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
value: {
default: 0,
},
},
}
</script>
<style lang="less" scoped>
.multi-select {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
.multi-title {
width: 100%;
height: 50px;
line-height: 50px;
text-align: center;
color: #878484;
font-weight: 300;
font-size: 14px;
}
.multi-main {
.btn-group {
display: inline-block;
width: 70px;
height: 70px;
margin-right: 15px;
.multi-icon {
width: 45px;
height: 45px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 50%;
margin: 0 auto;
border: 1px solid transparent;
&:hover {
color: red;
border-color: red;
background: transparent;
font-size: 18px;
}
}
p {
font-size: 12px;
margin-top: 8px;
text-align: center;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,239 @@
<template>
<div class="box" @scroll="scrollBottom">
<div class="tab">
<el-tabs v-model="activeName" @tab-click="handleClick" :stretch=true>
<el-tab-pane label="最近浏览" name="goods">
<dl>
<dd v-for="item in list" v-infinite-scroll="loadMore">
<div class="base">
<div>
<img :src="item.thumbnail" class="image" />
</div>
<div style="margin-left: 13px">
<a class="goods_name" @click="linkToGoods(item.goodsId, item.id)">{{ item.goodsName }}</a>
<div style="margin-top: 8px;">
<span style="color: red;">{{ item.price }}</span>
</div>
<div>
<el-button class="store-button" type="danger" v-if="item.btnHide == 1" size="mini"
@click="submitSendGoodsMessage(item)" plain>发送</el-button>
</div>
</div>
</div>
</dd>
</dl>
</el-tab-pane>
<el-tab-pane label="订单列表" name="orders">
<dl>
<dd v-for="(item, index) in orderList" v-infinite-scroll="loadMore" :key="index">
<div style="margin-bottom: 20px;">
<span class="orderSn">订单号{{ item.sn }}</span>
<img :src="item.groupImages" alt=""
style="height: 100px; width: 100px;margin-top: 10px; vertical-align: middle; ">
<span class="orderGoodsName" @click="linkToOrders(item.sn)"> {{ item.groupName }}</span>
<span style="margin-left: 10px; color: red;">{{ item.paymentTime }}</span>
<div class="orderBtn">
<el-button type="danger" class="store-button" v-if="item.btnHide == 1" size="mini"
@click="submitSendOrderMessage(item, index)" plain>发送</el-button>
</div>
</div>
</dd>
</dl>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script>
import { Tag, button, Tabs, TabPane, InfiniteScroll } from 'element-ui'
import { mapState, mapGetters } from "vuex";
export default {
directives: {
"infinite-scroll": InfiniteScroll,
},
data () {
return {
activeName: 'goods',
btnHide: undefined,
hide: true
}
},
computed: {
...mapGetters(["talkItems"]),
...mapState({
id: (state) => state.user.id,
toUser: (state) => state.user.toUser,
}),
},
components: {
"el-tag": Tag,
"el-button": button,
"el-tabs": Tabs,
"el-tab-pane": TabPane,
},
methods: {
//
scrollBottom (e) {
const { scrollTop, scrollHeight, clientHeight } = e.srcElement
if (scrollTop + clientHeight >= scrollHeight) {
this.$emit('loadMore')
}
},
loadMore () {
},
//
submitSendGoodsMessage (item) {
const context = {
id: item.id,
goodsId: item.goodsId,
thumbnail: item.thumbnail,
price: item.price,
goodsName: item.goodsName
}
const record = {
operation_type: "MESSAGE",
to: this.toUser.userId,
from: this.id,
message_type: "GOODS",
context: context,
talk_id: this.toUser.id,
};
this.$emit('sendMessage', record, context, 'GOODS');
localStorage.setItem(item.goodsId, 0)
item.btnHide = 0
},
//
submitSendOrderMessage (item, index) {
const context = {
sn: item.sn,
groupImages: item.groupImages,
paymentTime: item.paymentTime,
groupName: item.groupName,
}
const record = {
operation_type: "MESSAGE",
to: this.toUser.userId,
from: this.id,
message_type: "ORDER",
context: context,
talk_id: this.toUser.id,
};
this.$emit('sendMessage', record, context, 'ORDER');
localStorage.setItem(item.goodsId, 0)
item.btnHide = 0
},
handleClick (tab, event) {
console.log(tab, event);
}
},
props: {
list: {
type: Array,
default: [],
},
orderList: {
type: Array,
default: []
}
},
mounted () {
console.log(this.orderList, 'orderList');
this.btnHide = localStorage.getItem('btnHide')
}
}
</script>
<style scoped lang="less">
.orderGoodsName {
width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
position: absolute;
margin-left: 10px;
margin-top: 10px;
}
.orderSn {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.orderBtn {
display: flex;
justify-content: flex-end;
margin-right: 15px;
position: relative;
bottom: 30px;
}
.goods_name {
text-overflow: -o-ellipsis-lastline;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
/deep/ .el-tabs__item.is-top:last-child {
color: black;
}
/deep/.el-tabs__active-bar {
background-color: #f23030;
}
/deep/ .el-tabs__nav.is-top {
height: 50px;
}
.tab {
display: flex;
justify-content: center;
align-items: center;
}
.box {
height: 500px;
overflow: auto;
width: 350px;
}
.store-button {
background-color: white;
border-color: #F56C6C;
margin-top: 10px;
}
.base {
margin-top: 5px;
height: 120px;
display: flex;
.price {
color: red;
margin-top: 15px;
}
.image {
height: 100px;
margin-top: 3px;
width: 100px;
}
}
.separate {
margin-top: 8px;
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<div style="width: 350px;">
当前浏览
<div class="base">
<div>
<img :src="goodsDetail.thumbnail" class="image" />
</div>
<div style="margin-left: 13px">
<a @click="linkToGoods(goodsDetail.goodsId, goodsDetail.id)"> {{ goodsDetail.goodsName }} </a>
<div>
<span style="color: red;">{{ goodsDetail.price }}</span>
</div>
<div v-if="hide">
<el-button class="store-button" type="danger" v-if="!sendFlag && btnHide == 1" size="mini"
@click="submitSendMessage()" plain>发送</el-button>
</div>
</div>
</div>
<hr class="separate" />
</div>
</template>
<script>
import { Tag, button } from 'element-ui'
import { mapState, mapGetters } from "vuex";
import SocketInstance from "@/im-server/socket-instance";
export default {
data () {
return {
sendFlag: false,
btnHide: undefined,
hide: true
}
},
computed: {
...mapGetters(["talkItems"]),
...mapState({
id: (state) => state.user.id,
index_name: (state) => state.dialogue.index_name,
toUser: (state) => state.user.toUser,
}),
},
mounted () {
this.btnHide = localStorage.getItem('btnHide')
},
components: {
"el-tag": Tag,
"el-button": button,
Storage
},
methods: {
toGoods () {
alert("toGoods")
},
toMessage () {
alert(JSON.stringify(this.toUser))
alert("toMessage")
},
//
submitSendMessage () {
console.log("发送");
const context = this.goodsDetail
const record = {
operation_type: "MESSAGE",
to: this.toUser.userId,
from: this.id,
message_type: "GOODS",
context: context,
talk_id: this.toUser.id,
};
SocketInstance.emit("event_talk", record);
this.$store.commit("UPDATE_TALK_ITEM", {
index_name: this.index_name,
draft_text: "",
});
/**
* 插入数据
*/
const insterChat = {
createTime: this.formateDateAndTimeToString(new Date()),
fromUser: this.id,
toUser: record.to,
isRead: false,
messageType: "GOODS",
text: context,
float: "right",
};
console.log("insterChat", insterChat);
// console.log("",'')
//
this.$store.commit("PUSH_DIALOGUE", insterChat);
//
let el = document.getElementById("lumenChatPanel");
//
let isBottom =
Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight;
if (isBottom || record.to == this.id) {
this.$nextTick(() => {
el.scrollTop = el.scrollHeight;
});
} else {
this.$store.commit("SET_TLAK_UNREAD_MESSAGE", {
content: content,
nickname: record.name,
});
}
// 0 1
localStorage.setItem('btnHide', 0)
this.hide = false
},
formateDateAndTimeToString (date) {
var hours = date.getHours();
var mins = date.getMinutes();
var secs = date.getSeconds();
var msecs = date.getMilliseconds();
if (hours < 10) hours = "0" + hours;
if (mins < 10) mins = "0" + mins;
if (secs < 10) secs = "0" + secs;
if (msecs < 10) secs = "0" + msecs;
return (
this.formatDateToString(date) + " " + hours + ":" + mins + ":" + secs
);
},
formatDateToString (date) {
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
if (month < 10) month = "0" + month;
if (day < 10) day = "0" + day;
return year + "-" + month + "-" + day;
},
},
props: {
goodsDetail: {
type: Object,
default: null,
},
},
}
</script>
<style scoped lang="less">
.store-button {
background-color: white;
border-color: #F56C6C;
}
.base {
margin-top: 5px;
height: 120px;
display: flex;
div {
overflow: hidden;
text-overflow: ellipsis;
margin-top: 8px;
white-space: nowrap;
margin-top: 8px;
}
.image {
height: 100px;
margin-top: 3px;
width: 100px
}
}
.click-button {
margin-top: 8px;
background-color: white;
border-color: #F56C6C;
}
.separate {
margin-top: 8px;
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<div style="width: 362px;">
<div class="base">
<div>
<img :src="storeInfo.storeLogo" class="image" />
</div>
<div style="margin-left: 13px">
<div class="div-zoom">
{{ storeInfo.storeName }}
<el-tag type="danger" v-if="storeInfo.selfOperated" size="mini"></el-tag>
</div>
<div>
联系方式: {{ storeInfo.memberName }}
</div>
<div>
<el-button class="store-button" type="danger" @click="linkToStore(storeInfo.id)" size="mini"
plain>进入店铺</el-button>
</div>
</div>
</div>
<hr class="separate" />
<div class="separate">店铺评分: <span>{{ storeInfo.serviceScore }}</span></div>
<div class="separate">服务评分: <span>{{ storeInfo.descriptionScore }}</span></div>
<div class="separate">物流评分: <span>{{ storeInfo.deliveryScore }}</span></div>
</div>
</template>
<script>
import { Tag, button } from 'element-ui'
export default {
data () {
return {
}
},
components: {
"el-tag": Tag,
"el-button": button,
},
methods: {
},
props: {
storeInfo: {
type: Object,
default: null,
},
},
}
</script>
<style scoped lang="less">
.store-button {
background-color: white;
border-color: #F56C6C;
}
.base {
margin-top: 5px;
height: 120px;
display: flex;
div {
margin-top: 8px;
}
.image {
height: 100px;
margin-top: 3px;
width: 100px
}
}
.separate {
margin-top: 8px;
}
</style>

View File

@ -0,0 +1,506 @@
<template>
<div>
<el-container class="editor-container">
<el-header class="no-padding toolbar" height="35px">
<ul>
<li v-popover:popoverEmoticon>
<i class="iconfont icon-icon_im_face" style="font-size: 15px" />
<p class="tip-title">表情符号</p>
</li>
<!-- <li @click="codeBlock.isShow = true">
<i class="iconfont icon-daima" />
<p class="tip-title">代码片段</p>
</li>
<li @click="recorder = true">
<i class="el-icon-headset" />
<p class="tip-title">语音消息</p>
</li> -->
<!-- #TODO 发图片功能暂时隐藏 此处会涉及到token过期 -->
<!-- <li @click="$refs.restFile.click()">
<i class="el-icon-picture-outline-round" />
<p class="tip-title">图片</p>
</li> -->
<!-- <li @click="$refs.restFile2.click()">
<i class="el-icon-folder" />
<p class="tip-title">附件</p>
</li>
<li @click="filesManager.isShow = true">
<i class="el-icon-folder-opened" />
<p class="tip-title">上传管理</p>
</li>
<li v-show="isGroupTalk" @click="vote.isShow = true">
<i class="el-icon-s-data" />
<p class="tip-title">发起投票</p>
</li> -->
<!-- <p class="text-tips no-select">-->
<!-- <span>按Enter发送 / Shift+Enter 换行</span>-->
<!-- <el-popover placement="top-end" width="600" trigger="click">-->
<!-- <div class="editor-books">-->
<!-- <div class="books-title">编辑说明:</div>-->
<!-- <p>-->
<!-- 1.-->
<!-- 支持上传QQ及微信截图在QQ或微信中截图后使用Ctrl+v上传图片-->
<!-- </p>-->
<!-- <p>-->
<!-- 2.-->
<!-- 支持浏览器及Word文档中的图片复制上传复制后使用Ctrl+v上传图片-->
<!-- </p>-->
<!-- <p>3. 支持图片拖拽上传</p>-->
<!-- <p>4. 支持文件上传 ( 文件小于100M ) </p>-->
<!-- <p>5. 按Enter发送 / Shift+Enter 换行</p>-->
<!-- <p>-->
<!-- 6.-->
<!-- 注意当文件正在上传时请勿关闭网页或离开当前对话框否则将导致文件停止上传或上传失败-->
<!-- </p>-->
<!-- </div>-->
<!-- <i class="el-icon-info" slot="reference" />-->
<!-- </el-popover>-->
<!-- </p>-->
</ul>
<el-popover
ref="popoverEmoticon"
placement="top-start"
trigger="click"
width="300"
popper-class="no-padding el-popover-em"
>
<MeEditorEmoticon ref="editorEmoticon" @selected="selecteEmoticon" />
</el-popover>
<form
enctype="multipart/form-data"
style="display: none"
ref="fileFrom"
>
<input
type="file"
ref="restFile"
accept="image/*"
@change="uploadImageChange"
/>
<input type="file" ref="restFile2" @change="uploadFileChange" />
</form>
</el-header>
<el-main class="no-padding textarea">
<textarea
ref="textarea"
v-paste="pasteImage"
v-drag="dragPasteImage"
v-model.trim="editorText"
rows="6"
placeholder="你想要的聊点什么呢 ..."
@keydown="keydownEvent($event)"
@input="inputEvent($event)"
/>
</el-main>
</el-container>
<!-- 图片查看器 -->
<MeEditorImageView
ref="imageViewer"
v-model="imageViewer.isShow"
:file="imageViewer.file"
@confirm="confirmUploadImage"
/>
<MeEditorRecorder v-if="recorder" @close="recorder = false" />
<!-- 代码块编辑器 -->
<TalkCodeBlock
v-if="codeBlock.isShow"
:edit-mode="codeBlock.editMode"
@close="codeBlock.isShow = false"
@confirm="confirmCodeBlock"
/>
<!-- 文件上传管理器 -->
<MeEditorFileManage ref="filesManager" v-model="filesManager.isShow" />
<MeEditorVote
v-if="vote.isShow"
@close="
() => {
this.vote.isShow = false;
}
"
/>
</div>
</template>
<script>
import MeEditorEmoticon from "./MeEditorEmoticon";
import MeEditorFileManage from "./MeEditorFileManage";
import MeEditorImageView from "./MeEditorImageView";
import MeEditorRecorder from "./MeEditorRecorder";
import MeEditorVote from "./MeEditorVote";
import TalkCodeBlock from "@/components/chat/TalkCodeBlock";
import { getPasteImgs, getDragPasteImg } from "@/utils/editor";
import { findTalk } from "@/utils/talk";
import {
ServeSendTalkCodeBlock,
ServeSendTalkImage,
ServeSendEmoticon,
} from "@/api/chat";
export default {
name: "MeEditor",
components: {
MeEditorEmoticon,
MeEditorFileManage,
MeEditorImageView,
TalkCodeBlock,
MeEditorRecorder,
MeEditorVote,
},
computed: {
talkUser() {
return this.$store.state.dialogue.index_name;
},
isGroupTalk() {
return this.$store.state.dialogue.talk_type == 2;
},
},
watch: {
talkUser(n_index_name) {
this.$refs.filesManager.clear();
this.editorText = this.getDraftText(n_index_name);
},
},
data() {
return {
//
editorText: "",
//
imageViewer: {
isShow: false,
file: null,
},
codeBlock: {
isShow: false,
editMode: true,
},
filesManager: {
isShow: false,
},
vote: {
isShow: false,
},
//
recorder: false,
//
sendtime: 0,
// 1
interval: 1000,
};
},
methods: {
// 稿
getDraftText(index_name) {
console.log("findTalk(index_name)", findTalk(index_name));
return findTalk(index_name)?.draft_text || "";
},
//
pasteImage(e) {
let files = getPasteImgs(e);
if (files.length == 0) return;
this.openImageViewer(files[0]);
},
//
dragPasteImage(e) {
let files = getDragPasteImg(e);
if (files.length == 0) return;
this.openImageViewer(files[0]);
},
inputEvent(e) {
this.$emit("keyboard-event", e.target.value);
},
//
keydownEvent(e) {
if (e.keyCode == 13 && this.editorText == "") {
e.preventDefault();
}
//
if (e.keyCode == 13 && e.shiftKey == false && this.editorText != "") {
let currentTime = new Date().getTime();
if (this.sendtime > 0) {
// 1
if (currentTime - this.sendtime < this.interval) {
e.preventDefault();
return false;
}
}
this.$emit("send", this.editorText);
this.editorText = "";
this.sendtime = currentTime;
e.preventDefault();
}
},
//
uploadImageChange(e) {
this.openImageViewer(e.target.files[0]);
this.$refs.restFile.value = null;
},
//
uploadFileChange(e) {
let maxsize = 100 * 1024 * 1024;
if (e.target.files.length == 0) {
return false;
}
let file = e.target.files[0];
if (/\.(gif|jpg|jpeg|png|webp|GIF|JPG|PNG|WEBP)$/.test(file.name)) {
this.openImageViewer(file);
return;
}
if (file.size > maxsize) {
this.$notify.info({
title: "消息",
message: "上传文件不能大于100M",
});
return;
}
this.filesManager.isShow = true;
this.$refs.restFile2.value = null;
this.$refs.filesManager.upload(file);
},
//
openImageViewer(file) {
this.imageViewer.isShow = true;
this.imageViewer.file = file;
},
//
confirmCodeBlock(data) {
const { talk_type, receiver_id } = this.$store.state.dialogue;
ServeSendTalkCodeBlock({
talk_type,
receiver_id,
code: data.code,
lang: data.language,
}).then((res) => {
if (res.code == 200) {
this.codeBlock.isShow = false;
} else {
this.$notify({
title: "友情提示",
message: res.message,
type: "warning",
});
}
});
},
//
confirmUploadImage() {
let fileData = new FormData();
fileData.append("file", this.imageViewer.file);
let ref = this.$refs.imageViewer;
ServeSendTalkImage(fileData)
.then((res) => {
ref.loading = false;
if (res.code == 200) {
ref.closeBox();
} else {
this.$notify({
title: "友情提示",
message: res.message,
type: "warning",
});
}
})
.finally(() => {
ref.loading = false;
});
},
//
selecteEmoticon(data) {
if (data.type == 1) {
let value = this.editorText;
let el = this.$refs.textarea;
let startPos = el.selectionStart;
let endPos = el.selectionEnd;
let newValue =
value.substring(0, startPos) +
data.value +
value.substring(endPos, value.length);
this.editorText = newValue;
if (el.setSelectionRange) {
setTimeout(() => {
let index = startPos + data.value.length;
el.setSelectionRange(index, index);
el.focus();
}, 0);
}
} else {
const { talk_type, receiver_id } = this.$store.state.dialogue;
ServeSendEmoticon({
talk_type,
receiver_id,
emoticon_id: data.value,
});
}
this.$refs.popoverEmoticon.doClose();
},
},
};
</script>
<style scoped lang="less">
.editor-container {
height: 160px;
width: 100%;
background-color: white;
}
.editor-container .toolbar {
line-height: 35px;
border-bottom: 1px solid #f5f0f0;
border-top: 1px solid #f5f0f0;
}
.editor-container .toolbar li {
list-style: none;
float: left;
width: 35px;
margin-left: 3px;
cursor: pointer;
text-align: center;
line-height: 35px;
position: relative;
color: #8d8d8d;
}
.editor-container .toolbar li .tip-title {
display: none;
position: absolute;
top: 38px;
left: 0px;
height: 26px;
line-height: 26px;
background-color: rgba(31, 35, 41, 0.9);
color: white;
min-width: 30px;
font-size: 10px;
padding: 0 5px;
border-radius: 2px;
white-space: pre;
text-align: center;
user-select: none;
z-index: 1;
}
.editor-container .toolbar li:hover .tip-title {
display: block;
}
.editor-container .toolbar li:hover {
background-color: #f7f5f5;
}
.editor-container .toolbar .text-tips {
float: right;
margin-right: 15px;
font-size: 12px;
color: #ccc;
}
.editor-container .toolbar .text-tips i {
font-size: 14px;
cursor: pointer;
margin-left: 5px;
color: rgb(255, 181, 111);
}
.editor-container .textarea {
overflow: hidden;
position: relative;
}
textarea {
width: calc(100% - 10px);
width: -moz-calc(100% - 10px);
width: -webkit-calc(100% - 10px);
height: calc(100% - 10px);
height: -moz-calc(100% - 10px);
height: -webkit-calc(100% - 10px);
border: 0 none;
outline: none;
resize: none;
font-size: 15px;
overflow-y: auto;
color: #464545;
padding: 5px;
position: relative;
}
textarea::-webkit-scrollbar {
width: 4px;
height: 1px;
}
textarea::-webkit-scrollbar-thumb {
background: #d5cfcf;
}
textarea::-webkit-scrollbar-track {
background: #ededed;
}
textarea::-webkit-input-placeholder {
color: #dccdcd;
font-size: 12px;
font-weight: 400;
}
/* 编辑器文档说明 --- start */
.editor-books .books-title {
font-size: 16px;
height: 30px;
line-height: 22px;
margin-top: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #cbcbcb;
color: #726f6f;
font-weight: 400;
margin-left: 11px;
}
.editor-books p {
text-indent: 10px;
font-size: 12px;
height: 30px;
line-height: 30px;
color: #7f7c7c;
}
/* 编辑器文档说明 --- end */
</style>

View File

@ -0,0 +1,345 @@
<template>
<div>
<el-container class="container">
<el-main class="no-padding main lum-scrollbar">
<input
type="file"
ref="fileCustomEmoji"
accept="image/*"
style="display: none"
@change="customUploadEmoji"
/>
<div v-show="showEmoticonId == -1" class="emoticon">
<div class="title">QQ表情</div>
<div
v-for="(elImg, text) in emoji.emojis"
v-html="elImg"
:key="text"
class="emoticon-item"
@click="clickEmoticon(text)"
></div>
<div class="clear"></div>
<div class="title">符号表情</div>
<div
v-for="(item, i) in emoji.symbol"
:key="i"
class="emoticon-item symbol"
@click="clickEmoticon(item)"
>
{{ item }}
</div>
<div class="clear"></div>
</div>
<div
v-for="item in emojiItem.slice(1)"
v-show="item.emoticon_id == showEmoticonId"
:key="item.emoticon_id"
class="emoji-box"
>
<div
v-if="item.emoticon_id == 0"
class="emoji-item custom-emoji"
@click="$refs.fileCustomEmoji.click()"
>
<i class="el-icon-picture" />
<span>自定义</span>
</div>
<div
v-for="subitem in item.list"
:key="subitem.src"
class="emoji-item"
@click="clickImageEmoticon(subitem)"
>
<el-image :src="subitem.src" fit="cover" />
</div>
<div class="clear"></div>
</div>
</el-main>
<!-- <el-footer height="40px" class="no-padding footer">
<div class="toolbar-items">
<div
v-show="emojiItem.length > 13"
class="toolbar-item prev-page"
@click="turnPage(1)"
>
<i class="el-icon-caret-left" />
</div>
<div
v-for="(item, index) in showItems"
:key="index"
class="toolbar-item"
@click="triggerItem(item)"
>
<img :src="item.url" />
<p class="title">{{ item.name }}</p>
</div>
<div
v-show="emojiItem.length > 13 && showItems.length == 13"
class="toolbar-item next-page"
@click="turnPage(2)"
>
<i class="el-icon-caret-right" />
</div>
</div>
</el-footer> -->
</el-container>
<MeEditorSystemEmoticon
v-if="systemEmojiBox"
@close="systemEmojiBox = false"
/>
</div>
</template>
<script>
import MeEditorSystemEmoticon from "@/components/editor/MeEditorSystemEmoticon";
import { emojiList as emoji } from "@/utils/emojis";
import { mapState } from "vuex";
export default {
name: "MeEditorEmoticon",
components: {
MeEditorSystemEmoticon,
},
computed: {
...mapState({
emojiItem: (state) => state.emoticon.items,
}),
showItems() {
let start = (this.page - 1) * this.pageSize;
let end = start + this.pageSize;
return this.emojiItem.slice(start, end);
},
pageTotal() {
return this.emojiItem.length / this.pageSize;
},
},
data() {
return {
emoji,
//
systemEmojiBox: false,
showEmoticonId: -1,
showTitle: "QQ表情/符号表情",
page: 1,
pageSize: 13,
};
},
created() {
this.$store.commit("LOAD_USER_EMOTICON");
},
methods: {
//
turnPage(type) {
if (type == 1) {
if (this.page == 1) return false;
this.page--;
} else {
if (this.page >= this.pageTotal) return false;
this.page++;
}
},
//
triggerItem(item) {
this.showEmoticonId = item.emoticon_id;
this.showTitle = item.name;
},
//
clickEmoticon(emoji) {
this.callback({
type: 1,
value: emoji,
});
},
//
clickImageEmoticon(item) {
this.callback({
type: 2,
value: item.media_id,
});
},
callback(data) {
this.$emit("selected", data);
},
//
customUploadEmoji(e) {
if (e.target.files.length == 0) {
return false;
}
this.$store.commit("UPLOAD_USER_EMOTICON", {
file: e.target.files[0],
});
},
},
};
</script>
<style lang="less" scoped>
.container {
height: 300px;
max-width: 500px;
background-color: white;
.header {
line-height: 30px;
font-size: 13px;
font-weight: 400;
padding-left: 5px;
user-select: none;
position: relative;
border-bottom: 1px solid #fbf5f5;
.addbtn {
position: absolute;
right: 10px;
top: 1px;
color: #409eff;
cursor: pointer;
}
}
.footer {
background-color: #eff1f7;
.toolbar-items {
width: 100%;
height: 40px;
line-height: 40px;
display: flex;
flex-direction: row;
align-items: center;
.toolbar-item {
height: 30px;
width: 30px;
margin: 0 2px;
background-color: #fff;
display: inline-block;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
position: relative;
img {
width: 20px;
height: 20px;
}
.title {
display: none;
position: absolute;
top: -25px;
left: 0px;
height: 20px;
line-height: 20px;
background: #353434;
color: white;
min-width: 30px;
font-size: 10px;
padding-left: 5px;
padding-right: 5px;
border-radius: 2px;
white-space: pre;
text-align: center;
}
&:hover .title {
display: block;
}
}
}
}
}
.container .footer .toolbar-items .prev-page:active i,
.container .footer .toolbar-items .next-page:active i {
transform: scale(1.2);
}
.emoji-box,
.emoticon {
width: 100%;
}
.emoticon {
.title {
width: 50%;
height: 25px;
line-height: 25px;
color: #ccc;
font-weight: 400;
padding-left: 3px;
font-size: 12px;
}
.emoticon-item {
width: 30px;
height: 30px;
margin: 2px;
float: left;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover {
transform: scale(1.3);
}
}
.symbol {
font-size: 22px;
}
}
.emoji-box {
.emoji-item {
width: 67px;
height: 67px;
margin: 2px;
background-color: #eff1f7;
float: left;
cursor: pointer;
transition: ease-in 0.3s;
}
.custom-emoji {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
font-size: 10px;
i {
font-size: 30px;
margin-bottom: 3px;
}
&:active {
color: #409eff;
}
}
}
/deep/ .el-image {
width: 100%;
height: 100%;
transition: ease-in 0.3s;
}
.emoji-box .emoji-item:hover .el-image,
.emoji-box .emoji-item:hover {
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,440 @@
<template>
<el-container
class="container animated bounceInUp"
v-outside="closeBox"
v-if="show"
>
<el-header class="no-padding header" height="50px">
<p>
上传管理 <span v-show="total">({{ successNum }}/{{ total }})</span>
</p>
<i class="close-btn el-icon-close" @click="closeBox" />
</el-header>
<el-main class="no-padding mian lum-scrollbar">
<div class="empty-data" v-show="total == 0">
<SvgNotData />
<p>暂无上传文件</p>
</div>
<div
v-for="file in items"
v-show="!file.isDelete"
:key="file.hashName"
class="file-item"
>
<div class="file-header">
<div class="type-icon">{{ file.ext }}</div>
<el-tooltip :content="file.filename" placement="top-start">
<div class="filename">{{ file.filename }}</div>
</el-tooltip>
<div class="status">
<span v-if="file.status == 0"></span>
<span v-else-if="file.status == 1" style="color: #66b1ff">
正在上传...
</span>
<span v-else-if="file.status == 2" style="color: #67c23a">
已完成
</span>
<span v-else style="color: red">网络异常</span>
</div>
</div>
<div class="file-mian">
<div class="progress">
<el-progress
type="dashboard"
:percentage="file.progress"
:width="50"
:color="colors"
/>
<span class="name">上传进度</span>
</div>
<div class="detail">
<p>
文件类型<span>{{ file.filetype }}</span>
</p>
<p>
文件大小<span>{{ file.filesize }}</span>
</p>
<p>
上传时间<span>{{ file.datetime }}</span>
</p>
</div>
</div>
<div v-show="file.status == 2 || file.status == 3" class="file-means">
<div class="btns" @click="removeFile(file.hashName)"></div>
<div
v-show="file.status == 3"
class="btns"
@click="triggerUpload(file.hashName)"
>
继续上传
</div>
</div>
</div>
</el-main>
</el-container>
</template>
<script>
import Vue from 'vue'
import { SvgNotData } from '@/core/icons'
import { Progress } from 'element-ui'
Vue.use(Progress)
import { ServeFindFileSplitInfo, ServeFileSubareaUpload } from '@/api/upload'
import { formatSize, getFileExt, parseTime } from '@/utils/functions'
import { ServeSendTalkFile } from '@/api/chat'
export default {
name: 'MeEditorFileManage',
model: {
prop: 'show',
event: 'close',
},
props: {
show: Boolean,
},
components: {
SvgNotData,
},
data() {
return {
colors: [
{
color: '#f56c6c',
percentage: 20,
},
{
color: '#e6a23c',
percentage: 40,
},
{
color: '#5cb87a',
percentage: 60,
},
{
color: '#1989fa',
percentage: 80,
},
{
color: '#11ce65',
percentage: 100,
},
],
items: [],
}
},
computed: {
total() {
return this.items.filter(item => {
return item.isDelete === false
}).length
},
successNum() {
return this.items.filter(item => {
return item.isDelete === false && item.status == 2
}).length
},
},
methods: {
closeBox() {
this.$emit('close', false)
},
upload(file) {
ServeFindFileSplitInfo({
file_name: file.name,
file_size: file.size,
}).then(res => {
if (res.code == 200) {
const { hash_name, split_size } = res.data
this.items.unshift({
hashName: hash_name,
originalFile: file,
filename: file.name,
status: 0, // 0: 1: 2: 3:
progress: 0,
filesize: formatSize(file.size),
filetype: file.type || '未知',
datetime: parseTime(new Date(), '{m}-{d} {h}:{i}'),
ext: getFileExt(file.name),
forms: this.fileSlice(file, hash_name, split_size),
successNum: 0,
isDelete: false,
})
this.triggerUpload(hash_name)
}
})
},
//
fileSlice(file, hash, eachSize) {
const ext = getFileExt(file.name)
const splitNum = Math.ceil(file.size / eachSize) //
const forms = []
//
for (let i = 0; i < splitNum; i++) {
let start = i * eachSize
let end = Math.min(file.size, start + eachSize)
//
const form = new FormData()
form.append('file', file.slice(start, end))
form.append('name', file.name)
form.append('hash', hash)
form.append('ext', ext)
form.append('size', file.size)
form.append('split_index', i)
form.append('split_num', splitNum)
forms.push(form)
}
return forms
},
//
triggerUpload(hashName) {
let $index = this.getFileIndex(hashName)
if ($index < 0 || this.items[$index].isDelte) {
return
}
let i = this.items[$index].successNum
let form = this.items[$index].forms[i]
let length = this.items[$index].forms.length
this.items[$index].status = 1
ServeFileSubareaUpload(form)
.then(res => {
if (res.code == 200) {
$index = this.getFileIndex(hashName)
this.items[$index].successNum++
this.items[$index].progress = Math.floor(
(this.items[$index].successNum / length) * 100
)
if (this.items[$index].successNum == length) {
this.items[$index].status = 2
if (res.data.is_file_merge) {
ServeSendTalkFile({
hash_name: res.data.hash,
receiver_id: this.$store.state.dialogue.receiver_id,
talk_type: this.$store.state.dialogue.talk_type,
})
}
} else {
this.triggerUpload(hashName)
}
} else {
this.items[$index].status = 3
}
})
.catch(() => {
$index = this.getFileIndex(hashName)
this.items[$index].status = 3
})
},
//
getFileIndex(hashName) {
return this.items.findIndex(item => {
return item.hashName === hashName
})
},
removeFile(hashName) {
let index = this.getFileIndex(hashName)
this.items[index].isDelete = true
},
clear() {
this.items = []
},
},
}
</script>
<style lang="less" scoped>
.container {
position: fixed;
right: 0;
bottom: 0;
width: 400px;
height: 600px;
background-color: white;
box-shadow: 0 0 5px #eae5e5;
border: 1px solid #eae5e5;
overflow: hidden;
border-radius: 3px 3px 0 0;
.header {
height: 50px;
line-height: 50px;
position: relative;
text-indent: 20px;
border-bottom: 1px solid #f5eeee;
i {
position: absolute;
right: 20px;
top: 15px;
font-size: 20px;
cursor: pointer;
}
}
.mian {
.empty-data {
width: 100%;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 50%;
svg {
font-size: 70px;
}
p {
margin-top: 30px;
color: #cccccc;
font-size: 10px;
}
}
}
}
.file-item {
width: 95%;
min-height: 100px;
background-color: white;
display: flex;
flex-direction: column;
border-radius: 5px;
margin: 15px auto;
box-shadow: 0 0 5px #eae5e5;
overflow: hidden;
.file-header {
height: 45px;
display: flex;
flex-direction: row;
align-items: center;
position: relative;
border-bottom: 1px solid #f7f4f4;
.type-icon {
height: 30px;
width: 30px;
background-color: #66b1ff;
border-radius: 50%;
margin-left: 5px;
font-size: 10px;
font-weight: 200;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: white;
}
.filename {
margin-left: 10px;
font-size: 14px;
width: 65%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status {
position: absolute;
right: 14px;
top: 12px;
font-size: 13px;
color: #6b6868;
font-weight: 200;
}
}
.file-mian {
padding: 8px;
display: flex;
flex-direction: row;
.progress {
width: 80px;
height: 80px;
flex-shrink: 0;
background: #f9f6f6;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
cursor: pointer;
.name {
font-size: 12px;
color: #ada8a8;
font-weight: 300;
}
}
.detail {
flex: auto;
flex-shrink: 0;
display: flex;
flex-direction: column;
padding-left: 20px;
justify-content: center;
align-items: flex-start;
font-size: 13px;
p {
margin: 3px;
color: #ada8a8;
span {
color: #595a5a;
font-weight: 500;
}
}
}
}
.file-means {
width: 96.5%;
height: 35px;
border-top: 1px dashed rgb(234, 227, 227);
margin: 3px auto;
padding-top: 5px;
display: flex;
justify-content: flex-end;
align-items: center;
.btns {
width: 80px;
height: 25px;
border: 1px solid #e6e1e1;
display: flex;
justify-content: center;
align-items: center;
margin: 3px;
border-radius: 15px;
font-size: 12px;
color: #635f5f;
cursor: pointer;
&:active {
box-shadow: 0 0 5px #eae5e5;
font-size: 13px;
}
}
}
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div v-if="show" class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box" v-outside="closeBox">
<el-header class="no-padding header" height="50px">
<p>发送图片</p>
<p class="tools">
<i class="el-icon-close" @click="closeBox" />
</p>
</el-header>
<el-main class="no-padding mian">
<img v-show="src" :src="src" />
<div v-show="src">
<span class="filename">{{ fileName }}</span>
<br />
<span class="size">{{ fileSize }} KB</span>
</div>
</el-main>
<el-footer class="footer" height="50px">
<el-button
class="btn"
type="primary"
size="medium"
:loading="loading"
@click="uploadImage"
>立即发送
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
export default {
name: 'MeEditorImageView',
model: {
prop: 'show',
event: 'close',
},
props: {
show: Boolean,
file: File,
},
watch: {
file(file) {
this.loadFile(file)
},
},
data() {
return {
src: '',
fileSize: '',
fileName: '',
loading: false,
}
},
methods: {
closeBox() {
if (this.loading) {
return false
}
this.$emit('close', false)
},
loadFile(file) {
let reader = new FileReader()
this.fileSize = Math.ceil(file.size / 1024)
this.fileName = file.name
reader.onload = () => {
this.src = reader.result
}
reader.readAsDataURL(file)
},
//
uploadImage() {
this.loading = true
this.$emit('confirm')
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 450px;
.mian {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
img {
max-width: 80%;
max-height: 80%;
border-radius: 3px;
cursor: pointer;
box-shadow: 0 0 8px #e0dbdb;
}
div {
margin-top: 10px;
text-align: center;
overflow: hidden;
max-width: 80%;
.filename {
font-weight: 400;
}
.size {
color: rgb(148, 140, 140);
font-size: 12px;
}
}
}
.footer {
height: 50px;
background: rgba(247, 245, 245, 0.66);
text-align: center;
line-height: 50px;
.btn {
width: 150px;
border-radius: 2px;
}
}
}
</style>

View File

@ -0,0 +1,517 @@
<template>
<div class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box">
<el-header class="no-padding header no-select" height="50px">
<p>语音消息</p>
<p class="tools"><i class="el-icon-close" @click="closeBox" /></p>
</el-header>
<el-main class="no-padding mian">
<div class="music">
<span class="line line1" :class="{ 'line-ani': animation }"></span>
<span class="line line2" :class="{ 'line-ani': animation }"></span>
<span class="line line3" :class="{ 'line-ani': animation }"></span>
<span class="line line4" :class="{ 'line-ani': animation }"></span>
<span class="line line5" :class="{ 'line-ani': animation }"></span>
</div>
<div style="margin-top: 35px; color: #676262; font-weight: 300">
<template v-if="recorderStatus == 0">
<p style="font-size: 13px; margin-top: 5px">
<span>语音消息让聊天更简单方便 ...</span>
</p>
</template>
<template
v-else-if="
recorderStatus == 1 || recorderStatus == 2 || recorderStatus == 3
"
>
<p>{{ datetime }}</p>
<p style="font-size: 13px; margin-top: 5px">
<span v-if="recorderStatus == 1"></span>
<span v-else-if="recorderStatus == 2">已暂停录音</span>
<span v-else-if="recorderStatus == 3">录音时长</span>
</p>
</template>
<template
v-else-if="
recorderStatus == 4 || recorderStatus == 5 || recorderStatus == 6
"
>
<p>{{ formatPlayTime }}</p>
<p style="font-size: 13px; margin-top: 5px">
<span v-if="recorderStatus == 4"></span>
<span v-else-if="recorderStatus == 5">已暂停播放</span>
<span v-else-if="recorderStatus == 6">播放已结束</span>
</p>
</template>
</div>
</el-main>
<el-footer class="footer" height="50px">
<!-- 0:未开始录音 1:正在录音 2:暂停录音 3:结束录音 4:播放录音 5:停止播放 -->
<el-button
v-show="recorderStatus == 0"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="startRecorder"
>开始录音
</el-button>
<el-button
v-show="recorderStatus == 1"
type="primary"
size="mini"
round
icon="el-icon-video-pause"
@click="pauseRecorder"
>暂停录音
</el-button>
<el-button
v-show="recorderStatus == 2"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="resumeRecorder"
>继续录音
</el-button>
<el-button
v-show="recorderStatus == 2"
type="primary"
size="mini"
round
icon="el-icon-microphone"
@click="stopRecorder"
>结束录音
</el-button>
<el-button
v-show="recorderStatus == 3 || recorderStatus == 6"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="playRecorder"
>播放录音
</el-button>
<el-button
v-show="
recorderStatus == 3 || recorderStatus == 5 || recorderStatus == 6
"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="startRecorder"
>重新录音
</el-button>
<el-button
v-show="recorderStatus == 4"
type="primary"
size="mini"
round
icon="el-icon-video-pause"
@click="pausePlayRecorder"
>暂停播放
</el-button>
<el-button
v-show="recorderStatus == 5"
type="primary"
size="mini"
round
icon="el-icon-video-play"
@click="resumePlayRecorder"
>继续播放
</el-button>
<el-button
v-show="
recorderStatus == 3 || recorderStatus == 5 || recorderStatus == 6
"
type="primary"
size="mini"
round
@click="submit"
>立即发送
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
import Recorder from 'js-audio-recorder'
export default {
name: 'MeEditorRecorder',
data() {
return {
//
recorder: null,
//
duration: 0,
//
playTime: 0,
animation: false,
//
recorderStatus: 0, //0: 1: 2: 3: 4: 5: 6:
playTimeout: null,
}
},
computed: {
datetime() {
let hour = parseInt((this.duration / 60 / 60) % 24) //
let minute = parseInt((this.duration / 60) % 60) //
let seconds = parseInt(this.duration % 60) //
if (hour < 10) hour = `0${hour}`
if (minute < 10) minute = `0${minute}`
if (seconds < 10) seconds = `0${seconds}`
return `${hour}:${minute}:${seconds}`
},
formatPlayTime() {
let hour = parseInt((this.playTime / 60 / 60) % 24) //
let minute = parseInt((this.playTime / 60) % 60) //
let seconds = parseInt(this.playTime % 60) //
if (hour < 10) hour = `0${hour}`
if (minute < 10) minute = `0${minute}`
if (seconds < 10) seconds = `0${seconds}`
return `${hour}:${minute}:${seconds}`
},
},
destroyed() {
if (this.recorder) {
this.destroyRecorder()
}
},
methods: {
closeBox() {
if (this.recorder == null) {
this.$emit('close', false)
return
}
if (this.recorderStatus == 1) {
this.stopRecorder()
} else if (this.recorderStatus == 4) {
this.pausePlayRecorder()
}
//
this.destroyRecorder(() => {
this.$emit('close', false)
})
},
//
startRecorder() {
let _this = this
// http://recorder.api.zhuyuntao.cn/Recorder/event.html
// https://blog.csdn.net/qq_41619796/article/details/107865602
this.recorder = new Recorder()
this.recorder.onprocess = duration => {
duration = parseInt(duration)
_this.duration = duration
}
this.recorder.start().then(
() => {
this.recorderStatus = 1
this.animation = true
},
error => {
console.log(`${error.name} : ${error.message}`)
}
)
},
//
pauseRecorder() {
this.recorder.pause()
this.recorderStatus = 2
this.animation = false
},
//
resumeRecorder() {
this.recorderStatus = 1
this.recorder.resume()
this.animation = true
},
//
stopRecorder() {
this.recorderStatus = 3
this.recorder.stop()
this.animation = false
},
//
playRecorder() {
this.recorderStatus = 4
this.recorder.play()
this.playTimeouts()
this.animation = true
},
//
pausePlayRecorder() {
this.recorderStatus = 5
this.recorder.pausePlay()
clearInterval(this.playTimeout)
this.animation = false
},
//
resumePlayRecorder() {
this.recorderStatus = 4
this.recorder.resumePlay()
this.playTimeouts()
this.animation = true
},
//
destroyRecorder(callBack) {
this.recorder.destroy().then(() => {
this.recorder = null
if (callBack) {
callBack()
}
})
},
//
recorderSize() {
return this.recorder.fileSize
},
playTimeouts() {
this.playTimeout = setInterval(() => {
let time = parseInt(this.recorder.getPlayTime())
this.playTime = time
if (time == this.duration) {
clearInterval(this.playTimeout)
this.animation = false
this.recorderStatus = 6
}
}, 100)
},
submit() {
alert('功能研发中,敬请期待...')
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 500px;
max-width: 500px;
height: 450px;
.mian {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.footer {
height: 50px;
text-align: center;
line-height: 50px;
border-top: 1px solid #f7f3f3;
}
}
.music {
position: relative;
width: 180px;
height: 160px;
border: 8px solid #bebebe;
border-bottom: 0px;
border-top-left-radius: 110px;
border-top-right-radius: 110px;
}
.music:before,
.music:after {
content: '';
position: absolute;
bottom: -20px;
width: 40px;
height: 82px;
background-color: #bebebe;
border-radius: 15px;
}
.music:before {
right: -25px;
}
.music:after {
left: -25px;
}
.line {
position: absolute;
width: 6px;
min-height: 30px;
transition: 0.5s;
vertical-align: middle;
bottom: 0 !important;
box-shadow: inset 0px 0px 16px -2px rgba(0, 0, 0, 0.15);
}
.line-ani {
animation: equalize 4s 0s infinite;
animation-timing-function: linear;
}
.line1 {
left: 30%;
bottom: 0px;
animation-delay: -1.9s;
background-color: #ff5e50;
}
.line2 {
left: 40%;
height: 60px;
bottom: -15px;
animation-delay: -2.9s;
background-color: #a64de6;
}
.line3 {
left: 50%;
height: 30px;
bottom: -1.5px;
animation-delay: -3.9s;
background-color: #5968dc;
}
.line4 {
left: 60%;
height: 65px;
bottom: -16px;
animation-delay: -4.9s;
background-color: #27c8f8;
}
.line5 {
left: 70%;
height: 60px;
bottom: -12px;
animation-delay: -5.9s;
background-color: #cc60b5;
}
@keyframes equalize {
0% {
height: 48px;
}
4% {
height: 42px;
}
8% {
height: 40px;
}
12% {
height: 30px;
}
16% {
height: 20px;
}
20% {
height: 30px;
}
24% {
height: 40px;
}
28% {
height: 10px;
}
32% {
height: 40px;
}
36% {
height: 48px;
}
40% {
height: 20px;
}
44% {
height: 40px;
}
48% {
height: 48px;
}
52% {
height: 30px;
}
56% {
height: 10px;
}
60% {
height: 30px;
}
64% {
height: 48px;
}
68% {
height: 30px;
}
72% {
height: 48px;
}
76% {
height: 20px;
}
80% {
height: 48px;
}
84% {
height: 38px;
}
88% {
height: 48px;
}
92% {
height: 20px;
}
96% {
height: 48px;
}
100% {
height: 48px;
}
}
</style>

View File

@ -0,0 +1,169 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box" v-outside="closeBox">
<el-header class="no-padding header" height="50px">
<p>系统表情</p>
<p class="tools">
<i class="el-icon-close" @click="closeBox" />
</p>
</el-header>
<el-main class="no-padding mian lum-scrollbar">
<ul>
<li v-for="(item, i) in items" :key="item.id" class="no-select">
<div class="pkg-avatar">
<el-image :src="item.icon" fit="cover" :lazy="true" />
</div>
<div class="pkg-info" v-text="item.name"></div>
<div class="pkg-status">
<button
:class="{
'add-emoji': item.status == 0,
'remove-emoji': item.status != 0,
}"
@click="setEmoticon(i, item, item.status == 0 ? 1 : 2)"
>
{{ item.status == 0 ? '添加' : '移除' }}
</button>
</div>
</li>
</ul>
</el-main>
<el-footer class="footer" height="50px">
<el-button type="primary" size="medium" class="btn" @click="closeBox">
关闭窗口
</el-button>
</el-footer>
</el-container>
</div>
</template>
<script>
import { ServeFindSysEmoticon, ServeSetUserEmoticon } from '@/api/emoticon'
export default {
name: 'MeEditorSystemEmoticon',
data() {
return {
items: [],
}
},
created() {
this.loadSysEmoticon()
},
methods: {
closeBox() {
this.$emit('close')
},
//
loadSysEmoticon() {
ServeFindSysEmoticon().then(res => {
if (res.code == 200) {
this.items = res.data
}
})
},
setEmoticon(index, item, type) {
ServeSetUserEmoticon({
emoticon_id: item.id,
type: type,
}).then(res => {
if (res.code == 200) {
if (type == 1) {
this.items[index].status = 1
this.$store.commit('APPEND_SYS_EMOTICON', res.data)
} else {
this.items[index].status = 0
this.$store.commit('REMOVE_SYS_EMOTICON', {
emoticon_id: item.id,
})
}
}
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 350px;
max-width: 350px;
height: 500px;
.mian {
height: 480px;
overflow-y: auto;
li {
display: flex;
cursor: pointer;
height: 68px;
align-items: center;
border-bottom: 3px solid #fbf2fb;
padding-left: 5px;
.pkg-avatar {
flex-shrink: 0;
.el-image {
width: 50px;
height: 50px;
border-radius: 3px;
}
}
.pkg-info {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 14px;
margin-right: 14px;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
width: 200px;
color: #615d5d;
font-size: 13px;
}
.pkg-status {
flex-shrink: 0;
button {
font-size: 12px;
text-align: center;
line-height: 28px;
border-radius: 20px;
width: 50px;
cursor: pointer;
color: white;
}
.add-emoji {
background-color: #38adff;
}
.remove-emoji {
background-color: #ff5722;
}
}
}
}
.footer {
height: 50px;
background: rgba(247, 245, 245, 0.66);
text-align: center;
line-height: 50px;
.btn {
width: 150px;
border-radius: 2px;
}
}
}
</style>

View File

@ -0,0 +1,226 @@
<template>
<div class="lum-dialog-mask animated fadeIn">
<el-container class="lum-dialog-box">
<el-header class="header no-select" height="60px">
<p>发起投票</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close')" />
</p>
</el-header>
<el-main class="main no-padding vote-from">
<div class="vote-title">投票方式</div>
<div>
<el-radio-group v-model="mode">
<el-radio :label="0">单选</el-radio>
<el-radio :label="1">多选</el-radio>
</el-radio-group>
</div>
<div class="vote-title">投票主题</div>
<div>
<el-input
size="medium"
clear="vote-input"
v-model.trim="title"
placeholder="请输入投票主题最多50字"
:maxlength="50"
/>
</div>
<div class="vote-title">投票选项</div>
<div>
<div class="vote-options" v-for="(option, index) in options">
<div class="lbox">
<el-input
size="medium"
clear="vote-input"
v-model.trim="option.value"
placeholder="请输入选项内容"
:maxlength="120"
>
<span
slot="prefix"
style="margin-left:7px;"
v-text="String.fromCharCode(65 + index)"
/>
</el-input>
</div>
<div class="rbox">
<i class="el-icon-close" @click="removeOption(index)"></i>
</div>
</div>
<h6 class="pointer add-option" @click="addOption">
<i class="el-icon-plus"></i> 添加选项
</h6>
</div>
</el-main>
<el-footer class="footer">
<el-button plain size="small">取消</el-button>
<el-button
type="primary"
size="small"
:disabled="isCheck"
:loading="loading"
@click="submit"
>发起投票</el-button
>
</el-footer>
</el-container>
</div>
</template>
<script>
import { ServeSendVote } from '@/api/chat'
export default {
name: 'MeEditorVote',
props: {
group_id: {
type: [String, Number],
default: 0,
},
},
data() {
return {
loading: false,
mode: 0,
title: '',
options: [
{
value: '',
},
{
value: '',
},
{
value: '',
},
],
}
},
computed: {
isCheck() {
if (this.title == '') return true
return this.options.some(option => option.value == '')
},
},
methods: {
submit() {
let items = []
const { receiver_id } = this.$store.state.dialogue
this.options.forEach(option => {
items.push(option.value)
})
this.loading = true
ServeSendVote({
receiver_id,
mode: this.mode,
title: this.title,
options: items,
})
.then(res => {
if (res.code == 200) {
this.$emit('close')
this.$notify({
title: '友情提示',
message: '发起投票成功!',
type: 'success',
})
} else {
this.$notify({
title: '友情提示',
message: res.message,
type: 'warning',
})
}
})
.catch(() => {
this.loading = false
})
},
addOption() {
if (this.options.length >= 6) {
return false
}
this.options.push({
value: '',
})
},
removeOption(index) {
if (this.options.length <= 2) {
return false
}
this.$delete(this.options, index)
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
height: 600px;
max-width: 450px;
.vote-from {
box-sizing: border-box;
padding: 15px 25px;
overflow: hidden;
.vote-title {
margin: 20px 0 10px;
&:first-child {
margin-top: 0;
}
}
.vote-options {
display: flex;
min-height: 30px;
margin: 10px 0;
.lbox {
width: 100%;
/deep/.el-input__prefix {
height: 36px;
line-height: 36px;
}
}
.rbox {
flex-basis: 50px;
display: flex;
justify-content: center;
align-items: center;
i {
font-size: 18px;
cursor: pointer;
&:hover {
color: red;
}
}
}
}
.add-option {
margin-top: 5px;
font-weight: 400;
color: #3370ff;
}
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 32px;
}
}
/deep/.el-radio__input.is-checked + .el-radio__label {
color: #606266;
}
</style>

View File

@ -0,0 +1,18 @@
<template>
<img :src="'https://avatars.dicebear.com/api/initials/'+text+'.svg?fontSize=38'" alt=""/>
</template>
<script>
export default {
props:{
text:{
type:null,
default:''
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,18 @@
<template>
<img :src="text" alt=""/>
</template>
<script>
export default {
props:{
text:{
type:null,
default:''
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,49 @@
<template>
<div class="empty-content">
<div class="image">
<img :src="src" />
</div>
<div class="text" v-text="text" />
</div>
</template>
<script>
export default {
name: 'Empty',
props: {
text: {
type: String,
default: '数据为空...',
},
src: {
type: String,
default: require('@/assets/image/no-oncall.6b776fcf.png'),
},
},
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.empty-content {
width: 100%;
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 13px;
.image {
width: 200px;
height: 200px;
img {
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,313 @@
<template>
<div class="loading-content">
<div class="ant-spin ant-spin-lg ant-spin-spinning">
<span class="ant-spin-dot ant-spin-dot-spin">
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
<i class="ant-spin-dot-item" />
</span>
</div>
<p>{{ text }}</p>
</div>
</template>
<script>
export default {
name: 'Loading',
props: {
text: {
type: String,
default: '数据加载中 ...',
},
},
}
</script>
<style lang="less" scoped>
.loading-content {
width: 100%;
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 13px;
p {
margin-top: 10px;
}
}
/* ant-spin 加载动画 start */
.ant-spin {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
font-variant: tabular-nums;
line-height: 1.5715;
list-style: none;
-webkit-font-feature-settings: 'tnum';
font-feature-settings: 'tnum';
position: absolute;
display: none;
color: #1890ff;
text-align: center;
vertical-align: middle;
opacity: 0;
-webkit-transition: -webkit-transform 0.3s
cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: -webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
transition: transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86),
-webkit-transform 0.3s cubic-bezier(0.78, 0.14, 0.15, 0.86);
}
.ant-spin-spinning {
position: static;
display: inline-block;
opacity: 1;
}
.ant-spin-nested-loading {
position: relative;
}
.ant-spin-nested-loading > div > .ant-spin {
position: absolute;
top: 0;
left: 0;
z-index: 4;
display: block;
width: 100%;
height: 100%;
max-height: 400px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-dot {
position: absolute;
top: 50%;
left: 50%;
margin: -10px;
}
.ant-spin-nested-loading > div > .ant-spin .ant-spin-text {
position: absolute;
top: 50%;
width: 100%;
padding-top: 5px;
text-shadow: 0 1px 2px #fff;
}
.ant-spin-nested-loading > div > .ant-spin.ant-spin-show-text .ant-spin-dot {
margin-top: -20px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-dot {
margin: -7px;
}
.ant-spin-nested-loading > div > .ant-spin-sm .ant-spin-text {
padding-top: 2px;
}
.ant-spin-nested-loading > div > .ant-spin-sm.ant-spin-show-text .ant-spin-dot {
margin-top: -17px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-dot {
margin: -16px;
}
.ant-spin-nested-loading > div > .ant-spin-lg .ant-spin-text {
padding-top: 11px;
}
.ant-spin-nested-loading > div > .ant-spin-lg.ant-spin-show-text .ant-spin-dot {
margin-top: -26px;
}
.ant-spin-container {
position: relative;
-webkit-transition: opacity 0.3s;
transition: opacity 0.3s;
}
.ant-spin-container::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
display: none \9;
width: 100%;
height: 100%;
background: #fff;
opacity: 0;
-webkit-transition: all 0.3s;
transition: all 0.3s;
content: '';
pointer-events: none;
}
.ant-spin-blur {
clear: both;
overflow: hidden;
opacity: 0.5;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
.ant-spin-blur::after {
opacity: 0.4;
pointer-events: auto;
}
.ant-spin-tip {
color: rgba(0, 0, 0, 0.45);
}
.ant-spin-dot {
position: relative;
display: inline-block;
font-size: 20px;
width: 1em;
height: 1em;
}
.ant-spin-dot-item {
position: absolute;
display: block;
width: 9px;
height: 9px;
background-color: #1890ff;
border-radius: 100%;
-webkit-transform: scale(0.75);
transform: scale(0.75);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
opacity: 0.3;
-webkit-animation: antSpinMove 1s infinite linear alternate;
animation: antSpinMove 1s infinite linear alternate;
}
.ant-spin-dot-item:nth-child(1) {
top: 0;
left: 0;
}
.ant-spin-dot-item:nth-child(2) {
top: 0;
right: 0;
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ant-spin-dot-item:nth-child(3) {
right: 0;
bottom: 0;
-webkit-animation-delay: 0.8s;
animation-delay: 0.8s;
}
.ant-spin-dot-item:nth-child(4) {
bottom: 0;
left: 0;
-webkit-animation-delay: 1.2s;
animation-delay: 1.2s;
}
.ant-spin-dot-spin {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-animation: antRotate 1.2s infinite linear;
animation: antRotate 1.2s infinite linear;
}
.ant-spin-sm .ant-spin-dot {
font-size: 14px;
}
.ant-spin-sm .ant-spin-dot i {
width: 6px;
height: 6px;
}
.ant-spin-lg .ant-spin-dot {
font-size: 32px;
}
.ant-spin-lg .ant-spin-dot i {
width: 14px;
height: 14px;
}
.ant-spin.ant-spin-show-text .ant-spin-text {
display: block;
}
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.ant-spin-blur {
background: #fff;
opacity: 0.5;
}
}
@-webkit-keyframes antSpinMove {
to {
opacity: 1;
}
}
@keyframes antSpinMove {
to {
opacity: 1;
}
}
@-webkit-keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
@keyframes antRotate {
to {
-webkit-transform: rotate(405deg);
transform: rotate(405deg);
}
}
.ant-spin-rtl {
direction: rtl;
}
.ant-spin-rtl .ant-spin-dot-spin {
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-animation-name: antRotateRtl;
animation-name: antRotateRtl;
}
@-webkit-keyframes antRotateRtl {
to {
-webkit-transform: rotate(-405deg);
transform: rotate(-405deg);
}
}
@keyframes antRotateRtl {
to {
-webkit-transform: rotate(-405deg);
transform: rotate(-405deg);
}
}
/* ant-spin 加载动画 end */
</style>

View File

@ -0,0 +1,80 @@
<template>
<div>
<div class="abs-module" v-show="isShow">
<div class="abs-box">
<i class="el-icon-circle-close" @click="close" />
<a href="https://www.aliyun.com/minisite/goods?userCode=kqyyppx2">
<img src="~@/assets/image/aliyun-abs.jpg" width="300" />
</a>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false,
}
},
created() {
if (this.getNum() <= 2) {
setTimeout(() => {
this.isShow = true
}, 1000 * 60 * 2)
}
},
methods: {
getNum() {
return parseInt(sessionStorage.getItem('ABS_BOX')) || 0
},
close() {
sessionStorage.setItem('ABS_BOX', this.getNum() + 1)
this.isShow = false
},
},
}
</script>
<style lang="less" scoped>
.abs-module {
position: fixed;
width: 300px;
height: 163.63px;
right: 20px;
top: 20px;
border-radius: 5px;
z-index: 9999;
overflow: hidden;
transition: all 2s;
animation: absfade 1000ms infinite;
.abs-box {
width: 100%;
height: 100%;
position: relative;
i {
position: absolute;
right: 10px;
top: 10px;
color: white;
cursor: pointer;
font-size: 22px;
}
}
}
@keyframes absfade {
from {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
to {
transform: scale(1);
}
}
</style>

View File

@ -0,0 +1,246 @@
<template>
<div class="lum-dialog-mask">
<el-container class="lum-dialog-box">
<el-header class="header" height="50px">
<p>选择头像</p>
<p class="tools">
<i class="el-icon-close" @click="$emit('close', 0)" />
</p>
</el-header>
<el-main class="main">
<el-container class="full-height">
<el-aside width="400px">
<div class="cropper-box">
<vue-cropper
ref="cropper"
mode="cover"
:img="option.img"
:output-size="option.size"
:output-type="option.outputType"
:info="true"
:full="option.full"
:fixed="fixed"
:fixed-number="fixedNumber"
:can-move="option.canMove"
:can-move-box="option.canMoveBox"
:fixed-box="option.fixedBox"
:original="option.original"
:auto-crop="option.autoCrop"
:auto-crop-width="option.autoCropWidth"
:auto-crop-height="option.autoCropHeight"
:center-box="option.centerBox"
:high="option.high"
@real-time="realTime"
/>
<input
type="file"
id="uploads"
ref="fileInput"
accept="image/png, image/jpeg, image/jpg"
style="display: none"
@change="uploadImg($event, 1)"
/>
</div>
<div class="tools tools-flex">
<el-button
size="small"
plain
icon="el-icon-upload"
@click="clickUpload"
>上传图片
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh"
@click="refreshCrop"
>刷新
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh-left"
@click="rotateLeft"
>左转
</el-button>
<el-button
size="small"
plain
icon="el-icon-refresh-right"
@click="rotateRight"
>右转
</el-button>
</div>
</el-aside>
<el-main class="no-padding">
<div class="cropper-box">
<div class="preview-img">
<img v-if="cusPreviewsImg" :src="cusPreviewsImg" />
</div>
</div>
<div class="tools">
<el-button type="primary" size="small" @click="uploadService">
保存图片
</el-button>
</div>
</el-main>
</el-container>
</el-main>
</el-container>
</div>
</template>
<script>
import { VueCropper } from 'vue-cropper'
import { ServeUploadFileStream } from '@/api/upload'
export default {
name: 'AvatarCropper',
components: {
VueCropper,
},
data() {
return {
cusPreviewsImg: '',
previews: {},
option: {
img: '',
size: 1,
full: false,
outputType: 'png',
canMove: true,
fixedBox: true,
original: false,
canMoveBox: true,
autoCrop: true,
//
autoCropWidth: 200,
autoCropHeight: 150,
centerBox: false,
high: true,
},
fixed: true,
fixedNumber: [1, 1],
}
},
methods: {
clickUpload() {
this.$refs.fileInput.click()
},
clearCrop() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.clearCrop()
},
refreshCrop() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.refresh()
},
rotateLeft() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.rotateLeft()
},
rotateRight() {
if (!this.cusPreviewsImg) return false
this.$refs.cropper.rotateRight()
},
//
realTime() {
this.$refs.cropper.getCropData(img => {
this.cusPreviewsImg = img
})
},
//
uploadImg(e, num) {
let file = e.target.files[0]
if (!/\.(gif|jpg|jpeg|png|bmp|GIF|JPG|PNG)$/.test(e.target.value)) {
this.$message('图片类型必须是.gif,jpeg,jpg,png,bmp中的一种')
return false
}
let reader = new FileReader()
reader.onload = e => {
let data
if (typeof e.target.result === 'object') {
// Array Bufferblob base64
data = window.URL.createObjectURL(new Blob([e.target.result]))
} else {
data = e.target.result
}
if (num === 1) {
this.option.img = data
} else if (num === 2) {
this.example2.img = data
}
}
// base64
// reader.readAsDataURL(file)
// blob
reader.readAsArrayBuffer(file)
},
//
uploadService() {
if (this.cusPreviewsImg == '') return
ServeUploadFileStream({
fileStream: this.cusPreviewsImg,
})
.then(res => {
if (res.code == 200) {
this.$emit('close', 1, res.data.avatar)
} else {
this.$message('文件上传失败,请稍后再试...')
}
})
.catch(() => {
this.$message('文件上传失败,请稍后再试...')
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
height: 550px;
max-width: 800px;
.main {
.cropper-box {
height: 400px;
display: flex;
justify-content: center;
align-items: center;
.preview-img {
width: 180px;
height: 180px;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 0 4px #ccc;
img {
width: 100%;
height: 100%;
}
}
}
.tools {
height: 40px;
margin-top: 20px;
text-align: center;
button {
border-radius: 1px;
}
}
.tools-flex {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div>
<div class="reward" v-show="isShow">
<div class="title">
<span>Donate</span>
<i class="el-icon-circle-close" @click="close" />
</div>
<div class="main">
<div class="pay-box">
<img
src="https://cdn.learnku.com/uploads/images/202101/30/46424/PPYHOUhCb4.jpg"
/>
<p>支付宝</p>
</div>
<div class="pay-box">
<img
src="https://cdn.learnku.com/uploads/images/202101/30/46424/XLmCJjbvlQ.png"
/>
<p>微信</p>
</div>
</div>
<div class="footer">
开源不易如果你觉得项目对你有帮助可以请作者喝杯咖啡鼓励下...
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
isShow: false,
}
},
created() {
if (this.getNum() <= 3) {
setTimeout(() => {
this.isShow = true
}, 1000 * 30)
}
},
methods: {
getNum() {
return parseInt(localStorage.getItem('REWARD_BOX')) || 0
},
close() {
localStorage.setItem('REWARD_BOX', this.getNum() + 1)
this.isShow = false
},
},
}
</script>
<style lang="less" scoped>
.reward {
position: fixed;
width: 550px;
height: 400px;
right: 20px;
bottom: 20px;
border-radius: 5px;
box-shadow: 0 0 12px #ccc;
border: 1px solid rgb(228, 225, 225);
box-sizing: border-box;
overflow: hidden;
user-select: none;
z-index: 9999;
background: white;
.title {
height: 50px;
line-height: 50px;
padding-left: 20px;
width: 100%;
font-size: 16px;
background: #f9f7f7;
position: relative;
box-sizing: border-box;
i {
position: absolute;
right: 15px;
top: 18px;
font-size: 18px;
cursor: pointer;
}
}
.main {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.pay-box {
width: 200px;
height: 240px;
background: #1977ff;
margin: 0 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 5px;
img {
width: 150px;
height: 150px;
}
p {
margin-top: 20px;
color: white;
}
&:last-child {
background: #22ab38;
}
}
}
.footer {
height: 50px;
line-height: 50px;
text-align: center;
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<div></div>
</template>
<script>
//
export default {
data() {
return {
skins: [
{
theme: '',
class: 'default',
text: '默认',
},
{
theme: '',
class: 'red',
text: '红色',
},
{
theme: '',
class: 'dark',
text: '暗黑',
},
{
theme: '',
class: 'blue',
text: '浅蓝',
},
],
}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,44 @@
<template>
<div class="welcome-box">
<div class="famous-box">
<img src="~@/assets/image/chat.png" width="300" />
</div>
</div>
</template>
<script>
export default {
components: {},
data() {
return {}
},
created() {},
methods: {},
}
</script>
<style lang="less" scoped>
.welcome-box {
height: 100%;
width: 100%;
.famous-box {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100%;
font-size: 24px;
user-select: none;
p {
width: 100%;
font-weight: 300;
text-align: center;
font-size: 15px;
color: #b9b4b4;
margin-top: -30px;
}
}
}
</style>

View File

@ -0,0 +1,103 @@
<template>
<!-- 新消息提示组件 -->
<div class="notify-box">
<div class="lbox">
<el-avatar size="medium" :src="avatar" />
</div>
<div class="rbox">
<div class="xheader">
<p class="title">好友申请消息</p>
<p class="time">{{ datetime }}</p>
</div>
<div class="xbody">
<h4>申请备注:</h4>
<div>{{ content }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
components: {},
props: {
params: {
type: Object,
default() {},
},
},
data() {
return {
avatar:
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
talk_type: 1,
nickname: '阿萨纳斯卡',
content: '阿斯纳俺家你卡萨啊看看番按实际开发n',
datetime: '2021-06-18 23:15:12',
}
},
computed: {},
methods: {},
created() {},
}
</script>
<style lang="less" scoped>
.notify-box {
width: 300px;
min-height: 100px;
// background: rebeccapurple;
display: flex;
box-sizing: border-box;
padding: 5px;
.lbox {
flex-basis: 50px;
flex-shrink: 1;
display: flex;
justify-content: center;
}
.rbox {
flex: 1 auto;
margin-left: 5px;
.xheader {
height: 35px;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.title {
font-size: 13px;
font-weight: 500;
}
.time {
font-size: 12px;
}
}
.xbody {
min-height: 60px;
width: 100%;
h4 {
font-size: 13px;
font-weight: 400;
margin-bottom: 3px;
}
div {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
background: #f3f5f7;
font-size: 13px;
padding: 5px;
border-radius: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<!-- 新消息提示组件 -->
<div class="notify-box pointer">
<div class="lbox">
<el-avatar size="medium" shape="square" :src="avatar" />
</div>
<div class="rbox">
<div class="xheader">
<p class="title">
{{ talk_type == 1 ? '私信消息通知' : '群聊消息通知' }}
</p>
<p class="time"><i class="el-icon-time" /> {{ datetime | format }}</p>
</div>
<div class="xbody">
<p>@{{ nickname }}</p>
<div>{{ content }}</div>
</div>
</div>
</div>
</template>
<script>
import { parseTime } from '@/utils/functions'
export default {
components: {},
props: {
avatar: {
type: String,
default:
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
},
talk_type: {
type: Number,
default: 1,
},
nickname: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
datetime: {
type: String,
default: '',
},
},
data() {
return {}
},
filters: {
format(datetime) {
datetime = datetime || new Date()
return parseTime(datetime, '{m}/{d} {h}:{i} 分')
},
},
}
</script>
<style lang="less" scoped>
.notify-box {
width: 300px;
min-height: 100px;
display: flex;
box-sizing: border-box;
padding: 5px;
.lbox {
flex-basis: 50px;
flex-shrink: 1;
display: flex;
justify-content: center;
}
.rbox {
flex: 1 auto;
margin-left: 5px;
.xheader {
height: 25px;
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
.title {
font-size: 13px;
font-weight: 500;
}
.time {
font-size: 12px;
}
}
.xbody {
min-height: 60px;
width: 100%;
margin-top: 5px;
p {
font-size: 13px;
font-weight: 400;
color: #fb4208;
margin-bottom: 4px;
}
div {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
background: #f3f5f7;
font-size: 13px;
padding: 5px;
border-radius: 5px;
}
}
}
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'svg-icon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
},
}
</script>
<style scoped>
.svg-icon {
fill: currentColor;
overflow: hidden;
}
</style>

View File

@ -0,0 +1,119 @@
<template>
<div>
<div class="lum-dialog-mask" v-show="isShow">
<el-container class="lum-dialog-box" v-outside="close">
<el-header class="header" height="50px">
<p>添加好友</p>
<p class="tools">
<i class="el-icon-close" @click="close" />
</p>
</el-header>
<el-main class="main">
<el-input
v-model="mobile"
id="serach-mobile"
class="input"
prefix-icon="el-icon-search"
placeholder="请输入对方手机号(精确查找)"
clearable
@keyup.enter.native="onSubmit"
@input="error = false"
/>
<p v-show="error" class="error">
无法找到该用户请检查搜索内容并重试
</p>
<el-button
type="primary"
size="small"
:loading="loading"
@click="onSubmit"
>立即查找
</el-button>
</el-main>
</el-container>
</div>
</div>
</template>
<script>
import { ServeSearchContact } from '@/api/contacts'
export default {
name: 'UserSearch',
data() {
return {
loading: false,
isShow: false,
mobile: '',
error: false,
}
},
methods: {
//
open() {
this.mobile = ''
this.isShow = true
this.$nextTick(() => {
document.getElementById('serach-mobile').focus()
})
},
//
close() {
this.isShow = false
},
onSubmit() {
let { mobile } = this
if (mobile == '') return false
this.loading = true
ServeSearchContact({
mobile,
})
.then(res => {
if (res.code == 200) {
this.$user(res.data.id)
this.close()
} else {
this.error = true
}
})
.finally(() => {
this.loading = false
})
},
},
}
</script>
<style lang="less" scoped>
.lum-dialog-box {
width: 450px;
max-width: 450px;
height: 250px;
.main {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.input {
width: 85%;
}
.error {
width: 85%;
color: red;
font-size: 12px;
height: 50px;
line-height: 50px;
}
button {
margin-top: 20px;
width: 100px;
}
}
}
</style>

View File

@ -0,0 +1,483 @@
<template>
<div class="lum-dialog-mask animated fadeIn">
<el-container class="container" v-outside="close">
<el-header class="no-padding header" height="180px">
<i class="close el-icon-error pointer" @click="close" />
<div class="img-banner">
<img :src="detail.bag" class="img-banner" />
</div>
<div class="user-header">
<div class="avatar">
<div class="avatar-box">
<img :src="detail.avatar" :onerror="$store.state.defaultAvatar" />
</div>
</div>
<div class="nickname">
<i class="iconfont icon-qianming" />
<span>{{ detail.nickname || "未设置昵称" }}</span>
<div class="share no-select">
<i class="iconfont icon-fenxiang3" /> <span>分享</span>
</div>
</div>
</div>
</el-header>
<el-main class="no-padding main">
<div class="user-sign">
<div class="sign-arrow"></div>
<i class="iconfont icon-bianji" />
<span>编辑个签展示我的独特态度 </span>
</div>
<div class="card-rows no-select">
<div class="card-row">
<label>手机</label>
<span>{{ detail.mobile | mobile }}</span>
</div>
<div class="card-row">
<label>昵称</label>
<span>{{ detail.nickname || "未设置昵称" }}</span>
</div>
<div class="card-row">
<label>性别</label>
<span>{{ detail.gender | gender }}</span>
</div>
<div v-show="detail.friend_status == 2" class="card-row">
<label>备注</label>
<span v-if="editRemark.isShow == false">{{
detail.nickname_remark ? detail.nickname_remark : "暂无备注"
}}</span>
<span v-else>
<input
v-model="editRemark.text"
v-focus
class="friend-remark"
type="text"
@keyup.enter="editRemarkSubmit"
/>
</span>
<i
v-show="!editRemark.isShow"
class="el-icon-edit-outline"
@click="clickEditRemark"
/>
</div>
<div class="card-row">
<label>邮箱</label>
<span>未设置</span>
</div>
</div>
</el-main>
<el-footer
v-show="detail.friend_status !== 0"
class="no-padding footer"
height="50px"
>
<el-button
v-if="detail.friend_status == 1 && detail.friend_apply == 0"
type="primary"
size="small"
icon="el-icon-circle-plus-outline"
@click="apply.isShow = true"
>添加好友
</el-button>
<el-button
v-else-if="detail.friend_apply == 1"
type="primary"
size="small"
>已发送好友申请请耐心等待...
</el-button>
<el-button
v-else-if="detail.friend_status == 2"
type="primary"
size="small"
icon="el-icon-s-promotion"
@click="sendMessage(detail)"
>发消息
</el-button>
</el-footer>
<!-- 添加好友申请表单 -->
<div
v-outside="closeApply"
class="friend-from"
:class="{ 'friend-from-show': apply.isShow }"
>
<p>
<span>请填写好友申请备注</span>
<span @click="closeApply"></span>
</p>
<div>
<input
v-model="apply.text"
type="text"
placeholder="(必填项)"
@keyup.enter="sendApply"
/>
<el-button type="primary" size="small" @click="sendApply">
立即提交
</el-button>
</div>
</div>
</el-container>
</div>
</template>
<script>
// import { ServeSearchUser } from "@/api/user";
import { ServeCreateContact, ServeEditContactRemark } from "@/api/contacts";
import { toTalk } from "@/utils/talk";
export default {
name: "UserCardDetail",
props: {
user_id: {
type: Number,
default: 0,
},
},
filters: {
gender(value) {
let arr = ["未知", "男", "女"];
return arr[value] || "未知";
},
//
mobile(value) {
return (
value.substr(0, 3) + " " + value.substr(3, 4) + " " + value.substr(7, 4)
);
},
},
data() {
return {
detail: {
mobile: "",
nickname: "",
avatar: "",
motto: "",
friend_status: 0,
friend_apply: 0,
nickname_remark: "",
bag: require("@/assets/image/default-user-banner.png"),
gender: 0,
},
//
editRemark: {
isShow: false,
text: "",
},
//
apply: {
isShow: false,
text: "",
},
contacts: false,
};
},
created() {
// this.loadUserDetail();
},
methods: {
close() {
if (this.contacts) return false;
this.$emit("close");
},
//
clickEditRemark() {
this.editRemark.isShow = true;
this.editRemark.text = this.detail.nickname_remark;
},
//
// loadUserDetail() {
// ServeSearchUser({
// user_id: this.user_id,
// }).then((res) => {
// if (res.code == 200) {
// this.detail.user_id = res.data.id;
// Object.assign(this.detail, res.data);
// }
// });
// },
//
sendApply() {
if (this.apply.text == "") return;
ServeCreateContact({
friend_id: this.detail.user_id,
remark: this.apply.text,
}).then((res) => {
if (res.code == 200) {
this.apply.isShow = false;
this.apply.text = "";
this.detail.friend_apply = 1;
} else {
this.$message.error("发送好友申请失败,请稍后再试...");
}
});
},
//
editRemarkSubmit() {
let data = {
friend_id: this.detail.user_id,
remarks: this.editRemark.text,
};
if (data.remarks == this.detail.nickname_remark) {
this.editRemark.isShow = false;
return;
}
ServeEditContactRemark(data).then((res) => {
if (res.code == 200) {
this.editRemark.isShow = false;
this.detail.nickname_remark = data.remarks;
this.$emit("changeRemark", data);
}
});
},
//
closeApply() {
this.apply.isShow = false;
},
//
sendMessage() {
this.close();
toTalk(1, this.user_id);
},
},
};
</script>
<style lang="less" scoped>
.container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: white;
width: 350px;
height: 600px;
overflow: hidden;
border-radius: 3px;
.header {
position: relative;
.close {
position: absolute;
right: 10px;
top: 10px;
color: white;
transition: all 1s;
z-index: 1;
font-size: 20px;
}
.img-banner {
width: 100%;
height: 100%;
background-image: url(~@/assets/image/default-user-banner.png);
background-size: 100%;
transition: all 0.2s linear;
cursor: pointer;
overflow: hidden;
img:hover {
-webkit-transform: scale(1.1);
transform: scale(1.1);
-webkit-filter: contrast(130%);
filter: contrast(130%);
}
}
}
.main {
background-color: white;
padding: 45px 16px 0;
}
.footer {
display: flex;
justify-content: center;
align-items: center;
border-top: 1px solid #f5eeee;
button {
width: 90%;
}
}
}
.user-header {
width: 100%;
height: 80px;
position: absolute;
bottom: -40px;
display: flex;
flex-direction: row;
.avatar {
width: 100px;
flex-shrink: 0;
display: flex;
justify-content: center;
.avatar-box {
width: 80px;
height: 80px;
background-color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
img {
height: 70px;
width: 70px;
border-radius: 50%;
}
}
}
.nickname {
flex: auto;
padding-top: 50px;
font-size: 16px;
font-weight: 400;
span {
margin-left: 5px;
}
.share {
display: inline-flex;
width: 50px;
height: 22px;
background: #ff5722;
color: white;
align-items: center;
justify-content: center;
padding: 3px 8px;
border-radius: 20px;
transform: scale(0.7);
cursor: pointer;
i {
margin-top: 2px;
}
span {
font-size: 14px;
margin-left: 4px;
}
}
}
}
.user-sign {
min-height: 26px;
border-radius: 5px;
padding: 5px;
line-height: 25px;
background: #f3f5f7;
color: #7d7d7d;
font-size: 12px;
margin-bottom: 20px;
position: relative;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
position: relative;
.sign-arrow {
position: absolute;
width: 0;
height: 0;
font-size: 0;
border: 5px solid hsla(0, 0%, 96.9%, 0);
border-bottom-color: #f3f5f7;
left: 28px;
top: -9px;
}
}
.card-rows {
.card-row {
height: 35px;
line-height: 35px;
font-size: 14px;
position: relative;
cursor: pointer;
color: #736f6f;
label {
margin-right: 25px;
color: #cbc5c5;
}
.friend-remark {
border-bottom: 1px dashed #bec3d0;
padding-bottom: 2px;
color: #736f6f;
width: 60%;
padding-right: 5px;
}
.el-icon-edit-outline {
margin-left: 3px !important;
}
}
}
/* 好友申请表单 */
.friend-from {
position: absolute;
background: #fbf6f6;
height: 80px;
z-index: 2;
width: 100%;
bottom: -80px;
left: 0;
transition: all 0.5s ease-in-out;
p {
height: 20px;
line-height: 20px;
padding: 7px 5px 5px 15px;
font-size: 13px;
span {
&:nth-child(2) {
float: right;
margin-right: 13px;
color: #32caff;
cursor: pointer;
}
}
}
div {
height: 31px;
line-height: 20px;
padding: 7px 5px 5px 15px;
font-size: 13px;
}
input {
height: 30px;
line-height: 30px;
width: 220px;
border-radius: 3px;
padding: 0 5px;
margin-right: 5px;
}
}
.friend-from-show {
bottom: 0;
}
</style>

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