This commit is contained in:
全栈小学生 2024-12-12 18:33:37 +08:00
parent 60c28323dc
commit 614b592dcc
71 changed files with 2305 additions and 743 deletions

View File

@ -8,6 +8,7 @@
</template>
<script lang="ts" setup>
import { reactive, ref,computed,watch } from 'vue'
import useConfigStore from '@/stores/config'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import en from 'element-plus/dist/locale/en.mjs'
@ -16,23 +17,16 @@ import useAppStore from '@/stores/app'
import useMemberStore from '@/stores/member'
//
import '@/assets/styles/index.scss'
import { useRoute, useRouter } from 'vue-router'
if (process.client) {
const match = location.href.match(/\/web\/(\d*)\//)
const cookie = useCookie('siteId')
match ? cookie.value = match[1] : cookie.value = null
}
const router = useRouter()
//
const systemStore = useSystemStore()
const locale = computed(() => (systemStore.lang === 'zh-cn' ? zhCn : en))
//
const configStore = useConfigStore()
configStore.getLoginConfig()
//
systemStore.getSitenfo()
configStore.getLoginConfig(router)
//
getToken() && useMemberStore().setToken(getToken())
@ -57,11 +51,20 @@ watch(route, (nval, oval) => {
}, !oval ? 500 : 0)
}, { immediate: true })
// title
useHead({
titleTemplate: (title) => {
const siteTitle = systemStore.site.front_end_name || systemStore.site.site_name
return title ? `${title} - ${siteTitle}` : siteTitle
}
})
watch(() => systemStore.site, () => {
useHead({
titleTemplate: (title) => {
const siteTitle = systemStore.site.front_end_name || systemStore.site.site_name
if(title){
if(siteTitle){
return `${title} - ${siteTitle}`;
}else {
return title;
}
}else{
return siteTitle;
}
}
})
}, { deep: true, immediate: true })
</script>

View File

@ -1,27 +0,0 @@
/**
*
*/
export function getArticleList(params: Record<string, any>) {
return request.get('article/article', params)
}
/**
*
*/
export function getArticleAll(params: Record<string, any>) {
return request.get('article/article/all', params)
}
/**
*
*/
export function getArticleDetail(id: number) {
return request.get(`article/article/${id}`)
}
/**
*
*/
export function getArticleCategory() {
return request.get('article/category')
}

View File

@ -31,7 +31,7 @@ export function logout() {
*/
export function usernameRegister(data: AnyObject) {
let url = 'register'
data.pid && (url += `?pid=${data.pid}`)
data.pid && (url += `?pid=${ data.pid }`)
return request.post(url, data)
}
@ -40,7 +40,7 @@ export function usernameRegister(data: AnyObject) {
*/
export function mobileRegister(data: AnyObject) {
let url = 'register/mobile'
data.pid && (url += `?pid=${data.pid}`)
data.pid && (url += `?pid=${ data.pid }`)
return request.post(url, data)
}
@ -63,7 +63,7 @@ export function weappLogin(data: AnyObject) {
*/
export function bind(data: AnyObject) {
let url = 'bind'
data.pid && (url += `?pid=${data.pid}`)
data.pid && (url += `?pid=${ data.pid }`)
return request.post(url, data)
}
@ -79,4 +79,11 @@ export function scanlogin() {
*/
export function checkscan(data: AnyObject) {
return request.get('checkscan', data)
}
}
/**
*
*/
export function wechatCheck() {
return request.get('wechat/check')
}

View File

@ -6,7 +6,7 @@ export function getMemberInfo() {
*
*/
export function modifyMember(data: AnyObject) {
return request.put(`member/modify/${data.field}`, data)
return request.put(`member/modify/${ data.field }`, data)
}
/**
@ -16,6 +16,13 @@ export function getPointList(data: AnyObject) {
return request.get('member/account/point', data)
}
/**
*
*/
export function getMemberAccountPointcount() {
return request.get(`member/account/pointcount`)
}
/**
*
*/
@ -23,9 +30,23 @@ export function getBalanceList(data: AnyObject) {
return request.get('member/account/balance', data)
}
/**
*
*/
export function getBalanceListAll(data: AnyObject) {
return request.get('member/account/balance_list', data)
}
/**
*
*/
export function bindMobile(data: AnyObject) {
return request.put('member/mobile', data)
}
}
/**
*
*/
export function getMemberLevel() {
return request.get(`member/level`);
}

View File

@ -23,7 +23,7 @@ export function wechatSync(data: AnyObject) {
*
*/
export function getAgreementInfo(key: string) {
return request.get(`agreement/${key}`)
return request.get(`agreement/${ key }`)
}
/**
@ -37,7 +37,7 @@ export function resetPassword(data: AnyObject) {
*
*/
export function sendSms(data: AnyObject) {
return request.post(`send/mobile/${data.type}`, data)
return request.post(`send/mobile/${ data.type }`, data)
}
/**
@ -74,3 +74,23 @@ export function getCopyRight() {
export function getSiteInfo() {
return request.get('site')
}
/**
* 广
*/
export function getAdvInfo(params: Record<string, any>) {
return request.get(`web/adv`, params, { showErrorMessage: false })
}
/**
*
*/
export function getNavList() {
return request.get(`web/nav`)
}
/**
*
*/
export function getFriendlyLink() {
return request.get(`web/friendly_link`)
}

43
web/app/api/verify.ts Normal file
View File

@ -0,0 +1,43 @@
import request from '@/utils/request'
/**
*
*/
export function getVerifyCode(type: string, params: AnyObject) {
return request.get('verify', { type, data: params })
}
/**
*
*/
export function getVerifyRecords(params: Record<string, any>) {
return request.get('verify_records', params)
}
/**
*
*/
export function getCheckVerifier() {
return request.get('check_verifier')
}
/**
*
*/
export function getVerifierInfo(code: string) {
return request.get(`get_verify_by_code/${ code }`)
}
/**
*
*/
export function verify(code: string) {
return request.post(`verify/${ code }`, {}, { showSuccessMessage: true, showErrorMessage: true })
}
/**
*
*/
export function getVerifyDetail(code: string) {
return request.get(`verify_detail/${ code }`, {}, { showErrorMessage: true })
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,8 @@
{
"myBalance": "我的余额",
"accountType": "账户类型",
"changeInAmount": "金额变化",
"modeOfOccurrence": "发生方式",
"remark": "备注",
"occurrenceTime": "发生时间"
}

View File

@ -0,0 +1,15 @@
{
"personageInfo": "个人信息",
"memberHeadimg": "会员头像",
"edit": "修改",
"nickname": "会员昵称",
"username":"用户名",
"mobile":"手机号",
"updateMobile":"修改手机号",
"mobilePlaceholder": "请输入手机号",
"mobileTips":"请输入正确的手机号",
"mobileError": "请输入正确的手机号",
"codePlaceholder": "请输入手机验证码",
"cancel": "取消",
"confirm": "确定"
}

View File

@ -0,0 +1,8 @@
{
"myPoint": "我的积分",
"accountType": "账户类型",
"changeInAmount": "金额变化",
"modeOfOccurrence": "发生方式",
"remark": "备注",
"occurrenceTime": "发生时间"
}

View File

@ -1,5 +1,4 @@
{
"bind": "绑定",
"binding": "绑定中",
"usernamePlaceholder": "请输入账号",

View File

@ -14,7 +14,7 @@
"privacyAgreement": "隐私协议",
"protocolNotConfigured": "未配置协议",
"siteClose": "站点已关闭",
"noSite": "站点不存在",
"request": {
"unknownError": "未知错误",
"400": "错误的请求",
@ -32,4 +32,4 @@
"505": "http版本不支持该请求",
"timeout": "网络请求超时!"
}
}
}

View File

@ -3,6 +3,13 @@
"memberHeadimg": "会员头像",
"edit": "修改",
"nickname": "会员昵称",
"username":"用户名",
"mobile":"手机号",
"updateMobile":"修改手机号",
"mobilePlaceholder": "请输入手机号",
"mobileTips":"请输入正确的手机号",
"mobileError": "请输入正确的手机号",
"codePlaceholder": "请输入手机验证码",
"cancel": "取消",
"confirm": "确定"
}

View File

@ -5,5 +5,6 @@
"point": "积分",
"balance": "余额",
"looseChange": "零钱",
"notBound": "未绑定"
"notBound": "未绑定",
"coupon":"优惠劵"
}

View File

@ -1,20 +1,34 @@
{
"pages": {
"index": {
"index": "首页"
},
"auth": {
"login": "登录",
"register": "登录",
"bind": "手机号绑定"
},
"member": {
"index": "欢迎页",
"center": "个人中心"
},
"site": {
"close": "站点已关闭",
"nosite": "站点不存在"
}
"pages": {
"article": {
"list": "文章资讯",
"detail": "文章详情"
},
"index": {
"index": "首页"
},
"app":{
"index": "首页",
"member":{
"center": "个人中心",
"point": "我的积分",
"balance": "我的余额"
}
},
"auth": {
"agreement": "协议",
"login": "登录",
"register": "注册",
"bind": "手机号绑定"
},
"member": {
"index": "欢迎页",
"center": "个人中心",
"point": "我的积分",
"balance": "我的余额"
},
"site": {
"close": "站点已关闭"
}
}
}
}

View File

@ -1,78 +0,0 @@
<template>
<div class="w-full min-h-[100%] main-container pt-5">
<div class="mt-[20px] mb-[50px]" v-if="articleDeatail">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/article/list' }">文章</el-breadcrumb-item>
<el-breadcrumb-item >{{ articleDeatail.category_name }}</el-breadcrumb-item>
</el-breadcrumb>
<div >
<p class="py-[20px] text-center text-[24px]">{{ articleDeatail.title }}</p>
<div class="flex justify-center">
<!-- <div class="mr-3 flex items-center text-gray-500 text-sm text-[#999]"><el-icon><View /></el-icon> <span class="ml-1">浏览量158</span></div> -->
<div class="mr-3 flex items-center text-gray-500 text-sm text-[#999]"><el-icon><Clock /></el-icon><span class="ml-1">时间{{ articleDeatail.create_time }}</span></div>
</div>
<div class="mt-[50px]" v-html="articleDeatail.content"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { getArticleDetail } from '@/app/api/article'
import { ArrowRight } from '@element-plus/icons-vue'
import { nMounted } from 'vue';
import { useRoute } from 'vue-router';
const Route = useRoute(); //
const articleDeatail = ref();
onMounted(() => {
obtainArticleInfo(Route.query.id)
});
const obtainArticleInfo = (id) => {
getArticleDetail(id).then(res => {
articleDeatail.value = res.data;
})
}
</script>
<style lang="scss" scoped>
.index-carousel {
background-image: url('@/assets/images/index_carousel.png');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.article-wrap{
span{
line-height: 1;
box-shadow: 0 0 5px var(--el-color-primary-light-7);
&.active{
background-image: linear-gradient(to right,var(--el-color-primary-light-5), var(--el-color-primary));
}
&:hover{
background-image: linear-gradient(to right,var(--el-color-primary-light-5), var(--el-color-primary));
color: #fff;
}
}
}
.tow-line-overflow{
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.text-color{
color: var(--el-color-primary);
}
.custom-tabs-label span{
font-size: 20px;
padding: 0px 10px;
}
</style>

View File

@ -1,162 +0,0 @@
<template>
<div class="w-full main-container pt-5">
<el-carousel height="350px" indicator-position="none" arrow="never">
<el-carousel-item>
<div class="h-full index-carousel"></div>
</el-carousel-item>
</el-carousel>
<div class="mt-[20px] mb-[50px]">
<div>
<div class="w-full">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: '/article/list' }">文章</el-breadcrumb-item>
<el-breadcrumb-item v-if="selectedCategoryName">{{ selectedCategoryName }}</el-breadcrumb-item>
</el-breadcrumb>
<div class="flex mt-[20px] items-start">
<div class="w-[50px]">类目</div>
<el-row>
<el-button class="mb-[10px]" @click="selectedCategory(categoryItem)" v-for="(categoryItem, categoryIndex) in activeCategoryLsit" :key="categoryIndex">{{ categoryItem.name }}</el-button>
</el-row>
</div>
<div class="article-list mb-[20px] cursor-pointer" v-for="(activeItem, activeIndex) in articleTableData.data" :key="activeIndex" @click="toLink(activeItem.id)">
<div class="flex justify-between relative py-[20px] border-b-1 border-gray-300 border-solid">
<div class="w-[150px] h-[150px] flex items-center">
<img :src="img(activeItem.image)"/>
</div>
<div class="w-[1030px]">
<p class="text-xl font-bold">{{ activeItem.title }}</p>
<span class="overflow-ellipsis mt-2 mb-2 tow-line-overflow text-gray-500">{{ activeItem.intro }}</span>
</div>
<!-- <div class="activeBo flex items-right mt-2 justify-end absolute">
<span class="mr-5 text-sm text-gray-500">{{ activeItem.create_time }}</span>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><View /></el-icon> <span class="ml-1">158</span></div>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><Pointer /></el-icon> <span class="ml-1">22</span></div>
<div class="mr-3 flex items-center text-gray-500 text-sm"><el-icon><Star /></el-icon> <span class="ml-1">55</span></div>
<div class="flex items-center text-gray-500 text-sm"><el-icon><ChatDotRound /></el-icon> <span class="ml-1">655</span></div>
</div> -->
</div>
</div>
<el-pagination
class="justify-center"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
:page-size="articleTableData.limit"
background
layout="prev, pager, next"
:total="articleTableData.total"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { getArticleCategory,getArticleList } from '@/app/api/article'
import { ArrowRight } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router';
const router = useRouter();
const activeCategoryLsit = ref([])
const selectedCategoryName = ref()
const articleTableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: true,
data: [],
searchParam: {
title: '',
category_id:''
}
})
/**
* 获取文章列表
*/
const loadArticleList = (page: number = 1) => {
articleTableData.loading = true
articleTableData.page = page
getArticleList({
page: articleTableData.page,
limit: articleTableData.limit,
...articleTableData.searchParam
}).then(res => {
articleTableData.loading = false
articleTableData.data = res.data.data
articleTableData.total = res.data.total
}).catch(() => {
articleTableData.loading = false
})
}
loadArticleList()
const checkArticleCategory = () => {
getArticleCategory().then(res => {
activeCategoryLsit.value = res.data.data;
})
}
checkArticleCategory()
const selectedCategory = (item) => {
articleTableData.searchParam.category_id = item.category_id;
selectedCategoryName.value = item.name
}
const handleSizeChange = (val: number) => {
loadArticleList(val)
}
const handleCurrentChange = (val: number) => {
loadArticleList(val)
}
const toLink = (id) => {
router.push(`/article/detail?id=${id}`)
}
</script>
<style lang="scss" scoped>
.index-carousel {
background-image: url('@/assets/images/index_carousel.png');
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
}
.article-wrap{
span{
line-height: 1;
box-shadow: 0 0 5px var(--el-color-primary-light-7);
&.active{
background-image: linear-gradient(to right,var(--el-color-primary-light-5), var(--el-color-primary));
}
&:hover{
background-image: linear-gradient(to right,var(--el-color-primary-light-5), var(--el-color-primary));
color: #fff;
}
}
}
.tow-line-overflow{
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.text-color{
color: var(--el-color-primary);
}
.custom-tabs-label span{
font-size: 20px;
padding: 0px 10px;
}
.activeBo {
bottom : 20px;
right : 0px
}
</style>

View File

@ -1,29 +1,46 @@
<template>
<div class="w-full pt-6 min-h-[100%] flex flex-col justify-center">
<template v-if="agreement">
<div class="main-container" v-if="agreement.title && agreement.content">
<h2 class="text-center">{{ agreement.title }}</h2>
<div v-html="agreement.content"></div>
<div class="ml-[20px] min-h-[70vh] px-[20px] py-[30px] w-[1000px] bg-[#fff] rounded-[var(--rounded-big)]">
<div>
<div>
<template v-if="agreement">
<div class="" v-if="agreement.title && agreement.content">
<h2 class="text-center">{{ agreement.title }}</h2>
<div v-html="agreement.content"></div>
</div>
<el-empty :description="t('protocolNotConfigured')" :image-size="200" :image="img('static/resource/images/system/empty.png')" v-else />
</template>
</div>
<el-empty :description="t('protocolNotConfigured')" v-else />
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue'
import { getAgreementInfo } from '@/app/api/system'
import { useRoute } from 'vue-router';
const agreement = ref<any | null>(null)
const route = useRoute()
watch(() => route.query.key, (newVal, oldVal) => {
if(route.query.key){
getAgreementInfo(route.query.key).then(({ data }) => {
agreement.value = data
getAgreementInfo(route.query.key).then(({ data }) => {
agreement.value = data
if(data.title){
useHead({
title: data.title
})
}else {
useHead({
title: route.query.key == 'service' ? '用户协议' : '隐私协议'
})
}
}).catch(err => {
})
}
},{immediate: true})
useHead({
title: data.title
})
}).catch(err => {
})
</script>
<style lang="scss" scoped></style>

View File

@ -36,10 +36,12 @@ import { bind } from '@/app/api/auth'
import { bindMobile } from '@/app/api/member'
import useMemberStore from '@/stores/member'
import { FormInstance } from 'element-plus'
import { useRouter } from 'vue-router'
definePageMeta({
layout: "container"
});
let router = useRouter()
const memberStore = useMemberStore()
const info = computed(() => memberStore.info)
const loading = ref(false)
@ -88,7 +90,7 @@ const handleRegister = async () => {
request(formData).then((res: responseResult) => {
memberStore.setToken(res.data.token)
useLogin().handleLoginBack()
router.push({ path: '/' })
}).catch(() => {
loading.value = false
captcha.refresh()

View File

@ -1,21 +1,7 @@
<template>
<div class="w-full h-full bg-page flex items-center justify-center">
<div class="flex bg-white">
<div class="flex flex-col items-center w-[330px] py-[100px] border-r">
<div class="title font-bold text-xl">打开手机微信</div>
<div class="tips text-sm mt-[5px]">点击右上角打开扫一扫</div>
<div class="qrcode p-[10px] mt-[30px] border h-[120px] leading-none box-content">
<div class="relative">
<el-image :src="weixinCode.url" class="w-[120px]" />
<div class="flex flex-col justify-center items-center absolute inset-0 bg-gray-50" v-if="weixinCode.pastDue">
<span class="text-xs text-gray-600">{{ weixinCode.pastDueContent }}</span>
<span @click="scanLoginFn()" class="text-xs cursor-pointer text-color mt-2">点击刷新</span>
</div>
</div>
</div>
</div>
<div class="bg-white w-[380px] p-[30px]">
<div class="bg-white" v-if="loginType.length" >
<div class="bg-white w-[380px] p-[30px] h-[424px]" v-if="active">
<div class="flex items-end my-[30px]">
<div class="mr-[20px] text-base cursor-pointer leading-none" :class="{ 'font-bold': type == item.type }" v-for="item in loginType" @click="type = item.type">{{item.title }}</div>
</div>
@ -67,25 +53,57 @@
<span class="text-primary">{{ t('privacyAgreement') }}</span>
</NuxtLink>
</div>
<div class="mt-[20px] flex justify-center" v-if="show">
<span class="iconfont icon-weixin1 text-[#1AAD19] !text-[24px] cursor-pointer" @click="handleChange"></span>
</div>
</el-form>
</div>
<div class="flex flex-col items-center w-[380px] py-[60px] h-[424px]" v-else>
<div class="title font-bold text-xl">打开手机微信</div>
<div class="tips text-sm mt-[5px]">点击右上角打开扫一扫</div>
<div class="qrcode p-[10px] mt-[30px] border h-[120px] leading-none box-content">
<div class="relative">
<el-image :src="weixinCode.url" class="w-[120px]" />
<div class="flex flex-col justify-center items-center absolute inset-0 bg-gray-50" v-if="weixinCode.pastDue">
<span class="text-xs text-gray-600">{{ weixinCode.pastDueContent }}</span>
<span @click="scanLoginFn()" class="text-xs cursor-pointer text-color mt-2">点击刷新</span>
</div>
</div>
</div>
<div class="mt-[60px] text-base cursor-pointer leading-none" @click="handleChange">账号登录</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ref,reactive,watch,computed } from 'vue'
import { FormInstance } from 'element-plus'
import { usernameLogin, mobileLogin, scanlogin, checkscan } from '@/app/api/auth'
import { usernameLogin, mobileLogin, scanlogin, checkscan, wechatCheck } from '@/app/api/auth'
import { useRouter } from 'vue-router'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import QRCode from "qrcode";
definePageMeta({
layout: "container"
});
let router = useRouter()
let active = ref(true)
let timer:any = null
const handleChange = () => {
active.value = !active.value
if(!active.value){
scanLoginFn();
}else{
clearTimeout(timer)
}
}
watch(
() => router.currentRoute.value.path,
(toPath) => {
if (toPath != '/auth/login') {
clearTimeout(timer)
}
}, { immediate: true, deep: true }
)
//
const checkScanFn = (key) => {
let parameter = { key };
@ -94,18 +112,17 @@ const checkScanFn = (key) => {
let data = res.data;
switch (data.status) {
case 'wait':
setTimeout(() => {
timer = setTimeout(() => {
checkScanFn(weixinCode.value.key);
}, 1000);
break;
case 'success':
if (!data.login_data.token) {
useCookie('openId').value = data.login_data.openid
navigateTo(`/auth/bind`)
} else {
memberStore.setToken(data.login_data.token)
useLogin().handleLoginBack()
router.push({ path: '/' })
}
break;
case 'fail':
@ -139,7 +156,14 @@ const scanLoginFn = async () => {
checkScanFn(weixinCode.value.key);
}, 1000);
}
scanLoginFn();
let show = ref(false)
const wechatCheckFn = () =>{
wechatCheck().then((res:any) =>{
show.value = res.data
})
}
wechatCheckFn()
const memberStore = useMemberStore()
@ -153,7 +177,6 @@ const loginType = computed(() => {
type.value = value[0] ? value[0].type : ''
return value
})
const loading = ref(false)
const type = ref('')
const formData = reactive({
@ -209,7 +232,7 @@ const handleLogin = async () => {
login(formData).then(async (res) => {
await memberStore.setToken(res.data.token)
useLogin().handleLoginBack()
router.push({ path: '/app/index' })
}).catch(() => {
loading.value = false
})

View File

@ -1,30 +1,22 @@
<template>
<div class="w-full h-full bg-page flex items-center justify-center">
<div class="flex bg-white">
<div class="flex flex-col items-center w-[330px] py-[100px] border-r">
<div class="title font-bold text-xl">打开手机微信</div>
<div class="tips text-sm mt-[5px]">点击右上角打开扫一扫</div>
<div class="qrcode mt-[30px] border leading-none">
<el-image :src="img('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQHU7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAySlJSbU1Sb0hiMlQxOEcwSGhBY1AAAgTSfStkAwRYAgAA')" class="w-[120px]"></el-image>
</div>
</div>
<div class="bg-white w-[380px] p-[30px]">
<div class="bg-white w-[380px] p-[30px] h-[686px]" v-if="active">
<div class="flex items-end my-[30px]">
<div class="mr-[20px] text-base cursor-pointer leading-none" :class="{ 'font-bold': type == item.type }" v-for="item in registerType" @click="type = item.type">{{item.title }}</div>
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" :validate-on-rule-change="false">
<div v-show="type == 'username'">
<el-form-item prop="username">
<el-input v-model="formData.username" :placeholder="t('usernamePlaceholder')" clearable :inline-message="true">
<el-input v-model="formData.username" :placeholder="t('usernamePlaceholder')" clearable :inline-message="true" :readonly="real_name_input" @click="real_name_input = false" @blur="real_name_input = true">
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="formData.password" :placeholder="t('passwordPlaceholder')" type="password" clearable :show-password="true">
<el-input v-model="formData.password" :placeholder="t('passwordPlaceholder')" type="password" clearable :show-password="true" :readonly="password_input" @click="password_input = false" @blur="password_input = true">
</el-input>
</el-form-item>
<el-form-item prop="confirm_password">
<el-input v-model="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" type="password" clearable :show-password="true">
<el-input v-model="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" type="password" clearable :show-password="true" :readonly="confirm_password_input" @click="confirm_password_input = false" @blur="confirm_password_input = true">
</el-input>
</el-form-item>
</div>
@ -73,22 +65,36 @@
<span class="text-primary">{{ t('privacyAgreement') }}</span>
</NuxtLink>
</div>
<div class="mt-[20px] flex justify-center" v-if="show">
<span class="iconfont icon-weixin1 text-[#1AAD19] !text-[24px]" @click="active = !active"></span>
</div>
</el-form>
</div>
<div class="flex flex-col items-center w-[380px] py-[100px] h-[556px]" v-else>
<div class="title font-bold text-xl">打开手机微信</div>
<div class="tips text-sm mt-[5px]">点击右上角打开扫一扫</div>
<div class="qrcode mt-[30px] border leading-none">
<el-image :src="img('https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQHU7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAySlJSbU1Sb0hiMlQxOEcwSGhBY1AAAgTSfStkAwRYAgAA')" class="w-[120px]"></el-image>
</div>
<div class="mt-[60px] text-base cursor-pointer leading-none" @click="active = !active">账号注册</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { usernameRegister, mobileRegister } from '@/app/api/auth'
import { ref,reactive,watch,computed } from 'vue'
import { usernameRegister, mobileRegister, wechatCheck } from '@/app/api/auth'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import { FormInstance } from 'element-plus'
import { useRouter } from 'vue-router'
definePageMeta({
layout: "container"
});
let router = useRouter()
const memberStore = useMemberStore()
const configStore = useConfigStore()
@ -104,7 +110,7 @@ const registerType = computed(() => {
})
const loading = ref(false)
let active = ref(true)
const formData = reactive({
username: '',
password: '',
@ -188,7 +194,7 @@ const handleRegister = async () => {
register(formData).then((res: responseResult) => {
memberStore.setToken(res.data.token)
useLogin().handleLoginBack()
router.push({ path: '/' })
}).catch(() => {
loading.value = false
captcha.refresh()
@ -197,6 +203,14 @@ const handleRegister = async () => {
})
}
let show = ref(false)
const wechatCheckFn = () =>{
wechatCheck().then((res:any) =>{
show.value = res.data
})
}
wechatCheckFn()
//
const captcha = useCaptcha(formData)
captcha.refresh()
@ -210,6 +224,11 @@ const sendSmsCode = async () => {
}
})
}
const real_name_input = ref(true)
const password_input = ref(true)
const confirm_password_input = ref(true)
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template>
<div class="w-full">
<div class="w-full bg-[#fff]">
<el-carousel height="500px" arrow="never">
<el-carousel-item>
<div class="h-full index-carousel"></div>
@ -16,9 +16,8 @@
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/course.jpg" /></div>
<p class="text-[20px] text-[#666] font-bold">官方教程</p>
</div>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">
详尽细致的逐步官方教程帮助您系统全面的接触NIUCLOUD建议在使用前阅读</p>
<NuxtLink to="https://www.kancloud.cn/cui18734824089/niucloud-admin-develop/3148343" target="_blank">
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">详尽细致的逐步官方教程帮助您系统全面的接触NIUCLOUD建议在使用前阅读</p>
<NuxtLink to="https://doc.niucloud.com/v6" target="_blank">
<div class="flex justify-between items-center w-[280px] h-[40px] leading-[40px] rounded-[5px] border-[1px] border-[solid] border-[#508BFE]">
<span class="block ml-[20px] text-[14px] text-[#333]">前往教程</span>
<span class="block mr-[20px] text-[24px] text-[#333]"></span>
@ -30,23 +29,21 @@
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/api.jpg" /></div>
<p class="text-[20px] text-[#666] font-bold">API文档</p>
</div>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">
您可以通过API文档了解niucloud的正确使用方法也可以更加深入地理解niucloud的运行逻辑</p>
<NuxtLink to="https://www.niucloud.com/apidoc.html" target="_blank">
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">您可以通过API文档了解niucloud的正确使用方法也可以更加深入地理解niucloud的运行逻辑</p>
<!-- <NuxtLink to="https://www.niucloud.com/apidoc.html" target="_blank">-->
<div class="flex justify-between items-center w-[280px] h-[40px] leading-[40px] rounded-[5px] border-[1px] border-[solid] border-[#508BFE]">
<span class="block ml-[20px] text-[14px] text-[#333]">前往API文档</span>
<span class="block mr-[20px] text-[24px] text-[#333]"></span>
</div>
</NuxtLink>
<!-- </NuxtLink>-->
</div>
<div class="w-[280px]">
<div class="flex items-center">
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/community.jpg" /></div>
<p class="text-[20px] text-[#666] font-bold">问答社区</p>
</div>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">
便捷地浏览其它用户关于niucloud的问题并从解答中获取niucloud的使用方法当然您可以进行提问</p>
<NuxtLink>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">便捷地浏览其它用户关于niucloud的问题并从解答中获取niucloud的使用方法当然您可以进行提问</p>
<NuxtLink to="https://www.niushop.com/bbs.html" target="_blank">
<div class="flex justify-between items-center w-[280px] h-[40px] leading-[40px] rounded-[5px] border-[1px] border-[solid] border-[#508BFE]">
<span class="block ml-[20px] text-[14px] text-[#333]">前往问答社区</span>
<span class="block mr-[20px] text-[24px] text-[#333]"></span>
@ -58,8 +55,7 @@
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/wx.jpg"></div>
<p class="text-[20px] text-[#666] font-bold">关注公众号</p>
</div>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">
您可以扫描页面底部的二维码来关注我们的官方公众号获得一手咨询及使用技巧</p>
<p class="text-[14px] w-[280px] h-[100px] text-[#666666] leading-[22px] mt-[30px] mb-[20px]">您可以扫描页面底部的二维码来关注我们的官方公众号获得一手咨询及使用技巧</p>
</div>
</div>
@ -83,4 +79,4 @@
background-size: 100%;
background-repeat: no-repeat;
}
</style>
</style>

View File

@ -1,50 +1,78 @@
<template>
<div class="w-full h-full bg-page pt-6">
<div class="main-container flex justify-between" v-loading="balanceTableData.loading">
<sidebar></sidebar>
<el-card class="box-card flex-1 ml-4" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('myBalance')}}</span>
<div class="ml-[20px] min-h-[70vh] px-[20px] py-[30px] w-[1000px] bg-[#fff] rounded-[var(--rounded-big)]">
<div class="h-full" v-loading="balanceTableData.loading">
<div>
<div class="text-[18px] text-[#333] mb-[30px]">{{t('myBalance')}}</div>
<div class="text-center mb-[40px]">
<div class="text-[32px] text-[#333] font-600 mb-[10px]">{{ memberStore.info ? moneyFormat((parseFloat(memberStore.info.balance) + parseFloat(memberStore.info.money)).toString()) : '0.00' }}</div>
<div class="text-[#999] text-[14px]">账户余额()</div>
</div>
<div class="flex flex-wrap">
<div v-for="(item,index) in accountTypeList" :key="index" class="cursor-pointer relative text-[16px] mr-[50px] text-[#666] flex items-center justify-center" :class="{'class-select': item.key == balanceTableData.params.trade_type}" @click="fromTypeFn(item.key)">{{item.name}}</div>
</div>
<div v-if="balanceTableData.data.length && !balanceTableData.loading">
<div class="pt-[20px] px-[20px] rounded-[12px] min-h-[63%] mt-[30px] bg-[#fafafa]" >
<div >
<div class="flex items-center justify-between mb-[20px] border-b-[1px] border-dashed border-[#eee]" v-for="(item,index) in balanceTableData.data" :key="index">
<div>
<div class="font-[16px] mb-[14px] text-[#333]">{{ item.from_type_name }}</div>
<div class="text-[14px] text-[#999] mb-[20px]">{{ item.create_time }}</div>
</div>
<div class="text-right">
<div class="mb-[14px] text-[18px] price-font" :class="{'text-[#EF000C]' :item.account_data > 0, 'text-[#03B521]':item.account_data <= 0}">{{ item.account_data > 0 ? '+' + item.account_data : item.account_data }}</div>
<div class="text-[14px] text-[#999] mb-[20px]">余额 {{item.account_sum}}</div>
</div>
</div>
</div>
</div>
</template>
<div class="px-6">
<el-table :data="balanceTableData.data" stripe>
<el-table-column prop="account_type_name" :label="t('accountType')" width="180" />
<el-table-column prop="account_data" :label="t('changeInAmount')" width="120" />
<el-table-column prop="from_type_name" :label="t('modeOfOccurrence')" width="180" />
<el-table-column prop="memo" :label="t('remark')" width="180" />
<el-table-column prop="create_time" :label="t('occurrenceTime')" />
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="balanceTableData.page" v-model:page-size="balanceTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="balanceTableData.total" @size-change="loadBalanceList()" @current-change="loadBalanceList" />
<div class="mt-[30px] flex justify-center">
<el-pagination v-model:current-page="balanceTableData.page" v-model:page-size="balanceTableData.limit" layout="prev, pager, next" background hide-on-single-page :total="balanceTableData.total" @size-change="loadBalanceList()" @current-change="loadBalanceList" />
</div>
</div>
</el-card>
<div v-if="!balanceTableData.data.length && !balanceTableData.loading">
<el-empty description="暂无数据" :image-size="200" :image="img('static/resource/images/system/empty.png')"/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { UploadProps } from 'element-plus'
import { getBalanceList } from '@/app/api/member'
import useMemberStore from '@/stores/member'
import { moneyFormat } from '@/utils/common'
import { getBalanceListAll } from '@/app/api/member'
const memberStore = useMemberStore()
//
const accountTypeList = ref([
{name:'全部',key:''},
{name:'收入',key:'income'},
{name:'支出',key:'disburse'},
{name:'提现',key:'cash_out'},
])
//
const balanceTableData = reactive({
const balanceTableData = reactive<any>({
page: 1,
limit: 10,
total: 0,
loading: true,
data: []
data: [],
params:{
trade_type:''
}
})
const loadBalanceList = (page: number = 1) => {
balanceTableData.loading = true
balanceTableData.page = page
getBalanceList({
getBalanceListAll({
page: balanceTableData.page,
limit: balanceTableData.limit,
}).then(res => {
...balanceTableData.params
}).then((res: any) => {
balanceTableData.loading = false
balanceTableData.data = res.data.data
balanceTableData.total = res.data.total
@ -54,13 +82,27 @@ const loadBalanceList = (page: number = 1) => {
}
loadBalanceList()
//
const fromTypeFn = (key: string)=>{
balanceTableData.params.trade_type = key
loadBalanceList()
}
</script>
<style lang="scss" scoped>
.box-card{
border: none !important;
}
.text-color{
color: var(--jjext-color-brand);
}
.class-select{
color: var( --el-color-primary);
&::after{
background: #e93323;
content: "";
height: 2px;
position: absolute;
top: 31px;
width: 22px;
}
}
</style>

View File

@ -1,35 +1,47 @@
<template>
<div class="w-full h-full bg-page pt-6">
<div class="main-container flex justify-between">
<sidebar></sidebar>
<el-card class="box-card flex-1 ml-4" v-loading="loading" shadow="never">
<template #header>
<div class="card-header">
<span>{{ t('personageInfo') }}</span>
</div>
</template>
<div class="pr-15" v-if="info">
<el-form :model="info" class="form-wrap" label-width="120px">
<el-form-item :label="t('memberHeadimg')">
<div class="ml-[20px] min-h-[70vh] px-[20px] py-[30px] w-[1000px] bg-[#fff] rounded-[var(--rounded-big)]">
<div class="h-full" v-loading="loading">
<div>
<div class="text-[18px] text-[#333] mb-[50px]">我的信息</div>
<div v-if="info">
<el-form :model="info" class="form-wrap" label-width="120px" label-position="left">
<el-form-item :label="t('memberHeadimg')" class="pb-[20px] border-b-[1px] border-dashed border-[#ddd]">
<div class="w-full flex justify-between content-center items-center">
<img v-if="!info.headimg" class="w-[80px] h-[80px]" src="@/assets/images/default_headimg.png" alt="">
<img v-else :src="img(info.headimg)" class="w-[80px] h-[80px]" alt="">
<el-upload class="avatar-uploader" :show-file-list="false" v-bind="upload">
<span class="cursor-pointer text-color">{{ t('edit') }}</span>
<el-upload class="avatar-uploader" :show-file-list="false" v-bind="upload" ref="uploadRef">
<span class="cursor-pointer text-primary">{{ t('edit') }}</span>
</el-upload>
</div>
</el-form-item>
<el-form-item :label="t('nickname')">
<el-form-item :label="t('username')" class="pb-[20px] border-b-[1px] border-dashed border-[#ddd]">
<div class="w-full flex justify-between content-center">
<span>{{ updateNickname.value }}</span>
<span class="cursor-pointer text-color" @click="updateNickname.modal = true">{{ t('edit')}}</span>
<span>{{ info.username }}</span>
</div>
</el-form-item>
<el-form-item :label="t('nickname')" class="pb-[20px] border-b-[1px] border-dashed border-[#ddd]">
<div class="w-full flex justify-between content-center">
<div>
<span>{{ updateNickname.value }}</span>
<span v-if="currentLevel">(当前等级:{{currentLevel}})</span>
</div>
<span class="cursor-pointer text-primary" @click="updateNickname.modal = true">{{ t('edit')}}</span>
</div>
</el-form-item>
<el-form-item :label="t('mobile')" class="pb-[20px] border-b-[1px] border-dashed border-[#ddd]">
<div class="w-full flex justify-between content-center">
<span>{{ info.mobile }}</span>
<span v-if="!info.mobile" class="cursor-pointer text-primary" @click="updateMobileDialog = true">{{ t('edit')}}</span>
</div>
</el-form-item>
</el-form>
<div class="flex justify-end mt-[38px]">
<span class="cursor-pointer w-[130px] h-[40px] leading-[40px] text-center rounded-[4px] bg-[var(--el-color-primary)] text-white text-[14px]" @click="logoutFn">退出</span>
</div>
</div>
</el-card>
<el-dialog v-model="updateNickname.modal" :title="t('nickname')">
</div>
<!-- 更改昵称 -->
<el-dialog v-model="updateNickname.modal" :title="t('nickname')" width="380">
<el-form :model="info">
<el-form-item>
<el-input v-model="updateNickname.value" autocomplete="off" />
@ -42,6 +54,28 @@
</span>
</template>
</el-dialog>
<!-- 更改手机号码 -->
<el-dialog v-model="updateMobileDialog" :title="t('updateMobile')" width="420">
<el-form :model="formData" ref="formRef" :rules="formRules" :validate-on-rule-change="false">
<el-form-item prop="mobile">
<el-input v-model="formData.mobile" :placeholder="t('mobilePlaceholder')" clearable>
</el-input>
</el-form-item>
<el-form-item prop="mobile_code">
<el-input v-model="formData.mobile_code" :placeholder="t('codePlaceholder')">
<template #suffix>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key" @click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="updateMobileDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="mobileLoading" @click="updateMobileConfirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
@ -50,13 +84,12 @@
import { reactive, ref, computed } from 'vue'
import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
import { modifyMember } from '@/app/api/member'
import { ElMessage, UploadFile, UploadFiles } from 'element-plus'
import { modifyMember, bindMobile, getMemberLevel} from '@/app/api/member'
import { ElMessage, ElMessageBox, UploadFile, UploadFiles, FormInstance } from 'element-plus'
import request from '@/utils/request'
import storage from '@/utils/storage'
import { getToken } from '@/utils/common'
const memberStore = useMemberStore()
const loading = ref(true)
@ -66,14 +99,28 @@ const updateNickname = reactive({
value: ''
})
const info = computed(() => {
const info:any = computed(() => {
updateNickname.value = memberStore.info?.nickname;
if (memberStore.info) loading.value = false;
return memberStore.info;
})
const appStore = useAppStore()
definePageMeta({ middleware: 'auth' })
//
let currentLevel = ref('')
const getMemberLevelFn = () =>{
getMemberLevel().then((res:any) =>{
if(info.value && res.data && res.data.length){
res.data.forEach((item:any,index:number)=>{
if(item.level_id == info.value.member_level){
currentLevel.value = item.level_name
}
})
}
})
}
getMemberLevelFn()
const uploadRef = ref<any>(null)
const upload = computed(() => {
const headers: Record<string, any> = {}
headers.token = getToken()
@ -84,12 +131,13 @@ const upload = computed(() => {
headers,
onSuccess: (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
let img = uploadFile?.response?.data?.url;
if (response.code == 200) {
if (response.code == 200 || response.code == 1) {
modifyMember({
field: 'headimg',
value: img
}).then(() => {
memberStore.info.headimg = img
uploadRef.value.clearFiles()
})
} else {
uploadFile.status = 'fail'
@ -110,6 +158,84 @@ const updateNicknameConfirm = () => {
updateNickname.modal = false
})
}
//
const updateMobileDialog = ref(false)
const formData = reactive({
mobile: '',
mobile_code: '',
mobile_key: ''
})
const mobileLoading = ref(false)
const formRef = ref<FormInstance>()
const formRules = computed(() => {
return {
'mobile': [
{
required: true,
message: t('mobilePlaceholder'),
trigger: ['blur', 'change'],
},
{
validator(rule: any, value: string, callback: any) {
const phonePattern = /^1[3456789]\d{9}$/
if (!phonePattern.test(value)) {
return callback(new Error(t('mobileTips')))
} else {
return callback()
}
},
message: t('mobileError'),
trigger: ['blur'],
}
],
'mobile_code': {
required: true,
message: t('codePlaceholder'),
trigger: ['change']
}
}
})
const smsCodeRef = ref<AnyObject | null>(null)
const sendSmsCode = async () => {
await formRef.value?.validateField('mobile', async (valid, fields) => {
if (valid) {
smsCodeRef.value?.send()
}
})
}
const updateMobileConfirm = async () => {
await formRef.value?.validate(async (valid, fields) => {
if (valid) {
if (mobileLoading.value) return
mobileLoading.value = true
bindMobile(formData).then((res) => {
memberStore.getMemberInfo()
mobileLoading.value = false
updateMobileDialog.value = false
}).catch(() => {
mobileLoading.value = false
})
}
})
}
// 退
const logoutFn = () => {
ElMessageBox.confirm('您确定要退出账号吗?', '提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass:'!bg-[var(--el-color-primary)] !border-[var(--el-color-primary)]',
cancelButtonClass:'!border-[#dcdfe6]',
type: 'warning'
}
).then(() => {
memberStore.logout()
navigateTo(`/`)
})
}
</script>
<style lang="scss" scoped>

View File

@ -1,34 +1,64 @@
<template>
<div class="w-full h-full bg-page pt-6">
<div class="main-container flex justify-between" v-loading="pointTableData.loading">
<sidebar></sidebar>
<el-card class="box-card flex-1 ml-4" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('myPoint')}}</span>
<div class="ml-[20px] min-h-[70vh] px-[20px] py-[30px] w-[1000px] bg-[#fff] rounded-[var(--rounded-big)]">
<div class="h-full" v-loading="pointTableData.loading">
<div>
<div class="text-[18px] text-[#333] mb-[30px]">{{t('myPoint')}}</div>
<div class="flex justify-center mb-[40px]">
<div class="mr-[160px] text-center">
<div class="text-[32px] text-primary font-600 mb-[10px]">{{ pointInfo.point||0 }}</div>
<div class="text-[#999] text-[14px]">当前积分</div>
</div>
</template>
<div class="px-6">
<el-table :data="pointTableData.data" stripe>
<el-table-column prop="account_type_name" :label="t('accountType')" width="180" />
<el-table-column prop="account_data" :label="t('changeInAmount')" width="120" />
<el-table-column prop="from_type_name" :label="t('modeOfOccurrence')" width="180" />
<el-table-column prop="memo" :label="t('remark')" width="180" />
<el-table-column prop="create_time" :label="t('occurrenceTime')" />
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="pointTableData.page" v-model:page-size="pointTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="pointTableData.total" @size-change="loadPointList()" @current-change="loadPointList" />
<div class="mr-[160px] text-center">
<div class="text-[32px] text-[#333] font-600 mb-[10px]">{{ pointInfo.point_get||0 }}</div>
<div class="text-[#999] text-[14px]">累计积分</div>
</div>
<div class="mr-[160px] text-center">
<div class="text-[32px] text-[#333] font-600 mb-[10px]">{{ pointInfo.use||0 }}</div>
<div class="text-[#999] text-[14px]">累计消费</div>
</div>
<div class="text-center">
<div class="text-[32px] text-[#333] font-600 mb-[10px]">{{ pointInfo.point||0 }}</div>
<div class="text-[#999] text-[14px]">可用积分</div>
</div>
</div>
</el-card>
<div v-if="pointTableData.data.length && !pointTableData.loading">
<div class="pt-[20px] px-[20px] rounded-[12px] min-h-[70%] mt-[30px] bg-[#fafafa]">
<div>
<template v-for="(item,index) in pointTableData.data" :key="index">
<div class="flex items-center justify-between mb-[20px] border-b-[1px] border-dashed border-[#eee]" v-for="(subItem,subIndex) in item.month_data" :key="subIndex">
<div>
<div class="font-[16px] mb-[14px] text-[#333]">{{ subItem.from_type_name }}</div>
<div class="text-[14px] text-[#999] mb-[20px]">{{ subItem.create_time }}</div>
</div>
<div class="mb-[14px] text-[18px] price-font" :class="{'text-[#EF000C]' : subItem.account_data > 0, 'text-[#03B521]':subItem.account_data <= 0}">{{ subItem.account_data > 0 ? '+' + subItem.account_data : subItem.account_data }}</div>
</div>
</template>
</div>
</div>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="pointTableData.page" v-model:page-size="pointTableData.limit" layout="prev, pager, next" background hide-on-single-page :total="pointTableData.total" @size-change="loadPointList()" @current-change="loadPointList" />
</div>
</div>
<div v-if="!pointTableData.data.length && !pointTableData.loading">
<el-empty description="暂无数据" :image-size="200" :image="img('static/resource/images/system/empty.png')"/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { UploadProps } from 'element-plus'
import { getPointList } from '@/app/api/member'
import { getPointList, getMemberAccountPointcount } from '@/app/api/member'
//
const pointInfo = ref({});
const getMemberAccountPointcountFn = () =>{
getMemberAccountPointcount().then((res: any) =>{
pointInfo.value = res.data
})
}
getMemberAccountPointcountFn()
//
const pointTableData = reactive({
page: 1,
@ -44,7 +74,7 @@ const loadPointList = (page: number = 1) => {
getPointList({
page: pointTableData.page,
limit: pointTableData.limit,
}).then(res => {
}).then((res: any) => {
pointTableData.loading = false
pointTableData.data = res.data.data
pointTableData.total = res.data.total

View File

@ -3,6 +3,10 @@ export default [
path: "/",
component: () => import('~/app/pages/index.vue')
},
{
path: "/app/index",
component: () => import('~/app/pages/index.vue')
},
{
path: "/auth/login",
component: () => import('~/app/pages/auth/login.vue'),
@ -26,34 +30,40 @@ export default [
},
{
path: "/auth/agreement",
component: () => import('~/app/pages/auth/agreement.vue')
},
{
path: "/member",
component: () => import('~/app/pages/member/index.vue'),
component: () => import('~/app/pages/auth/agreement.vue'),
meta: {
middleware: ["auth"]
layout: "member"
}
},
{
path: "/member/center",
path: "/app/auth/agreement",
component: () => import('~/app/pages/auth/agreement.vue'),
meta: {
layout: "member"
}
},
{
path: "/app/member/center",
component: () => import('~/app/pages/member/center.vue'),
meta: {
middleware: ["auth"]
middleware: ["auth"],
layout: "member"
}
},
{
path: "/member/balance",
path: "/app/member/balance",
component: () => import('~/app/pages/member/balance.vue'),
meta: {
middleware: ["auth"]
middleware: ["auth"],
layout: "member"
}
},
{
path: "/member/point",
path: "/app/member/point",
component: () => import('~/app/pages/member/point.vue'),
meta: {
middleware: ["auth"]
middleware: ["auth"],
layout: "member"
}
},
{
@ -62,12 +72,5 @@ export default [
meta: {
layout: "container"
}
},
{
path: "/site/nosite",
component: () => import('~/app/pages/site/nosite.vue'),
meta: {
layout: "container"
}
}
]

View File

@ -1,6 +1,6 @@
<template>
<div class="w-screen h-screen flex flex-col items-center justify-center">
<el-empty :description="t('siteClose')" :image="img('static/resource/images/site/close.png')" image-size="300px" />
<div class="w-screen h-screen bg-[#fff] flex flex-col items-center justify-center">
<el-empty :description="t('siteClose')" :image="img('static/resource/images/site/close.png')" image-size="200px" />
</div>
</template>

View File

@ -1,14 +0,0 @@
<template>
<div class="w-screen h-screen flex flex-col items-center justify-center">
<el-empty :description="t('noSite')" :image="img('static/resource/images/site/close.png')" image-size="300px" />
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "container"
});
</script>
<style lang="scss" scoped></style>

View File

@ -5,7 +5,8 @@ const addonRoutes = import.meta.globEager('@/addon/**/pages/routes.ts')
for (const key in addonRoutes) {
const addon = key.split('/')[2]
routes.push(...addonRoutes[key].default.map((item) => {
// 先加载插件路由后加载app路由
routes.unshift(...addonRoutes[key].default.map((item) => {
item.meta = item.meta ? Object.assign(item.meta, { addon }) : { addon }
return item
}))

Binary file not shown.

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
web/assets/images/user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,64 @@
@font-face {
font-family: 'oppoSans-M';
src: url('@/assets/styles/OPPOSans-M.ttf') format('truetype');
}
@font-face {
font-family: 'oppoSans-R';
src: url('@/assets/styles/OPPOSans-R.ttf') format('truetype');
}
@font-face {
font-family: 'myFont';
src: url('@/assets/styles/custom.ttf') format('truetype');
}
.oppoSans-M{
font-family: 'oppoSans-M';
font-weight: 500;
}
.oppoSans-R{
font-family: 'oppoSans-R';
}
.price-font{
font-family: 'myFont';
}
body{
font-family: 'oppoSans-M';
background-color: #f5f5f5;
color: #303133;
}
div{
box-sizing: border-box;
}
// 标签
.tag-item{
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
font-size: 12px;
border-radius: 6px;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.main-container {
width: 1200px;
margin: 0 auto;
}
}
/* 多行超出隐藏 */
.multi-hidden {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.el-button:focus-visible{
outline: none !important;
}

Binary file not shown.

View File

@ -1,178 +1,455 @@
@font-face {
font-family: "iconfont"; /* Project id 4174881 */
src: url('//at.alicdn.com/t/c/font_4174881_3g0m7bfru3s.woff2?t=1703736772336') format('woff2'),
url('//at.alicdn.com/t/c/font_4174881_3g0m7bfru3s.woff?t=1703736772336') format('woff'),
url('//at.alicdn.com/t/c/font_4174881_3g0m7bfru3s.ttf?t=1703736772336') format('truetype');
}
.iconfont {
src: url('//at.alicdn.com/t/c/font_4174881_kzu1qah5r4j.woff2?t=1733801795296') format('woff2'),
url('//at.alicdn.com/t/c/font_4174881_kzu1qah5r4j.woff?t=1733801795296') format('woff'),
url('//at.alicdn.com/t/c/font_4174881_kzu1qah5r4j.ttf?t=1733801795296') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-shoucang:before {
}
.icon-gouwucheV6xx6:before {
content: "\e835";
}
.icon-zuoV6xx:before {
content: "\e630";
}
.icon-a-guanbi34:before {
content: "\e82b";
}
.icon-zujiV6xx:before {
content: "\e765";
}
.icon-gouwucheV6xx-2:before {
content: "\e6fe";
}
.icon-woV6xx1:before {
content: "\e709";
}
.icon-mima:before {
content: "\e65e";
}
.icon-shoujiV6xx:before {
content: "\e7a1";
}
.icon-tubiaoV6-3:before {
content: "\e664";
}
.icon-youV6xx:before {
content: "\e631";
}
.icon-riliV6xx:before {
content: "\e73e";
}
.icon-xiugaiV6xx:before {
content: "\e659";
}
.icon-shanchu-yuangaizhiV6xx:before {
content: "\e6e7";
}
.icon-shenhezhong:before {
content: "\e6cb";
}
.icon-jishibenV6xx:before {
content: "\e6fb";
}
.icon-fapiao:before {
content: "\e658";
}
.icon-checkbox_nol:before {
content: "\e62d";
}
.icon-shangV6xx-1:before {
content: "\e641";
}
.icon-xiaV6xx:before {
content: "\e643";
}
.icon-aixin1:before {
content: "\e663";
}
.icon-fujian:before {
content: "\e611";
}
.icon-group-num:before {
content: "\e603";
}
.icon-zhaopian:before {
content: "\e618";
}
.icon-duanxinV6xx1:before {
content: "\e654";
}
.icon-aixin:before {
content: "\e62a";
}
.icon-paixujiantoushang:before {
content: "\e60d";
}
.icon-paixujiantouxia:before {
content: "\e60e";
}
.icon-xuanze1:before {
content: "\e616";
}
.icon-weixinzhifu:before {
content: "\e650";
}
.icon-zhifubaozhifu:before {
content: "\e627";
}
.icon-yuezhifu:before {
content: "\e629";
}
.icon-shijian_o:before {
content: "\ebb1";
}
.icon-shangjiantou:before {
content: "\e79d";
}
.icon-xiajiantou1:before {
content: "\e79e";
}
.icon-shouye:before {
content: "\e626";
}
.icon-Vector-25:before {
content: "\e70b";
}
.icon-youhuiquan:before {
content: "\e623";
}
.icon-ruzhushenqing:before {
content: "\e624";
}
.icon-dizhiguanli:before {
content: "\e625";
}
.icon-shoucang1:before {
content: "\e620";
}
.icon-shoucang2:before {
content: "\e621";
}
.icon-fanhui:before {
content: "\e61c";
}
.icon-sousuo:before {
content: "\e61e";
}
.icon-gouwuche1:before {
content: "\e61f";
}
.icon-yanjing_kai:before {
content: "\e61b";
}
.icon-shijian:before {
content: "\e619";
}
.icon-bofang:before {
content: "\e606";
}
.icon-weixin1:before {
content: "\e6ea";
}
.icon-gengduo-lan:before {
content: "\e615";
}
.icon-shoucang01:before {
content: "\ea33";
}
.icon-a-dingdan2:before {
content: "\eba3";
}
.icon-a-qian2:before {
content: "\eb44";
}
.icon-caigou6:before {
content: "\eba1";
}
.icon-a-zhibao5:before {
content: "\ebc9";
}
.icon-shizhong:before {
content: "\e74f";
}
.icon-zhanghaodenglu:before {
content: "\e698";
}
.icon-mianxing_denglu_erweimadenglu:before {
content: "\e6fd";
}
.icon-yishoucang:before {
content: "\e60c";
}
.icon-dizhi-tianjia:before {
content: "\e622";
}
.icon-xuanzhong4:before {
content: "\e9ec";
}
.icon-jiageshaixuanshang:before {
content: "\e612";
}
.icon-shangjiashijian:before {
content: "\e684";
}
.icon-kefu:before {
content: "\e657";
}
.icon-xiangshangjiantou:before {
content: "\e65d";
}
.icon-dijia:before {
content: "\e601";
}
.icon-zhifa:before {
content: "\e648";
}
.icon-hanghuo:before {
content: "\e61a";
}
.icon-pinzhong:before {
content: "\e62b";
}
.icon-gouwuche:before {
content: "\e652";
}
.icon-shoucang:before {
content: "\e600";
}
.icon-geren:before {
}
.icon-geren:before {
content: "\e610";
}
.icon-c:before {
}
.icon-c:before {
content: "\e683";
}
.icon-wenhao:before {
}
.icon-wenhao:before {
content: "\e628";
}
.icon-jiahao:before {
}
.icon-jiahao:before {
content: "\e602";
}
.icon-biaoqian:before {
}
.icon-biaoqian:before {
content: "\e63d";
}
.icon-xiajiantou:before {
}
.icon-xiajiantou:before {
content: "\e63c";
}
.icon-rili1:before {
}
.icon-rili1:before {
content: "\e62f";
}
.icon-huangguan:before {
}
.icon-huangguan:before {
content: "\e6bb";
}
.icon-05_success:before {
}
.icon-05_success:before {
content: "\e6b2";
}
.icon-huojian1:before {
}
.icon-huojian1:before {
content: "\e6c0";
}
.icon-shouquanliebiao:before {
}
.icon-shouquanliebiao:before {
content: "\e696";
}
.icon-icon_huojian:before {
}
.icon-icon_huojian:before {
content: "\e694";
}
.icon-xiaoxi1:before {
}
.icon-xiaoxi1:before {
content: "\e695";
}
.icon-huojian:before {
}
.icon-huojian:before {
content: "\e693";
}
.icon-tishi2:before {
}
.icon-tishi2:before {
content: "\e6fc";
}
.icon-kaifazhe:before {
}
.icon-kaifazhe:before {
content: "\e692";
}
.icon-xiaoxi:before {
}
.icon-xiaoxi:before {
content: "\e690";
}
.icon--_shengchengyanshi:before {
}
.icon--_shengchengyanshi:before {
content: "\e68f";
}
.icon-tishi1:before {
}
.icon-tishi1:before {
content: "\e691";
}
.icon-dian:before {
}
.icon-dian:before {
content: "\ec1e";
}
.icon-huanyingye:before {
}
.icon-huanyingye:before {
content: "\e68e";
}
.icon-gerenxinxi:before {
}
.icon-gerenxinxi:before {
content: "\e6f4";
}
.icon-shimingrenzheng:before {
}
.icon-shimingrenzheng:before {
content: "\e6f9";
}
.icon-kaifashangzhongxin:before {
}
.icon-kaifashangzhongxin:before {
content: "\e6fa";
}
.icon-wodezhanghu:before {
}
.icon-wodezhanghu:before {
content: "\e6f8";
}
.icon-wodeshouquan:before {
}
.icon-wodeshouquan:before {
content: "\e6f6";
}
.icon-wodezhandian:before {
}
.icon-wodezhandian:before {
content: "\e6f5";
}
.icon-wodedingdan:before {
}
.icon-wodedingdan:before {
content: "\e6f2";
}
.icon-lingdang-xianxing:before {
}
.icon-lingdang-xianxing:before {
content: "\e8c0";
}
.icon-weixin:before {
}
.icon-weixin:before {
content: "\e62c";
}
.icon-fenxiang:before {
}
.icon-fenxiang:before {
content: "\e86e";
}
.icon-erweima:before {
}
.icon-erweima:before {
content: "\e680";
}
.icon-shoujihao:before {
}
.icon-shoujihao:before {
content: "\e62e";
}
.icon-tishi:before {
}
.icon-tishi:before {
content: "\e613";
}
.icon-shimingrenzheng-xian:before {
}
.icon-shimingrenzheng-xian:before {
content: "\e89c";
}
.icon-icon-selected:before {
}
.icon-icon-selected:before {
content: "\e61d";
}
.icon-yangshi_icon_tongyong_shield:before {
}
.icon-yangshi_icon_tongyong_shield:before {
content: "\e668";
}
.icon-xiangyoujiantou:before {
}
.icon-xiangyoujiantou:before {
content: "\e65f";
}
.icon-xiangzuojiantou:before {
}
.icon-xiangzuojiantou:before {
content: "\e660";
}
.icon-qiye:before {
}
.icon-qiye:before {
content: "\e60f";
}
}

View File

@ -1,2 +1,21 @@
@import 'iconfont.css';
@import 'common.scss';
@import 'common.scss';
// 主色调修改
:root{
--el-color-primary: #EF000C;
--el-color-primary-light-3: #EF000C;
--el-color-primary-light-5: #f49991;
--el-color-primary-light-7: #f8c2bd;
--el-color-primary-light-8: #fbd6d3;
--el-color-primary-light-9: #fff;
--el-color-primary-dark-2: #EF000C;
--el-price:#E4221C;
// 圆角大小
--rounded-xl: 100px;
--rounded-big: 16px;
--rounded-mid: 12px;
--rounded-med: 8px;
--rounded-small: 6px;
}

View File

@ -0,0 +1,76 @@
<template>
<div class="loginPopup">
<el-dialog v-model="dialogVisible" align-center width="430" :before-close="beforeClose" custom-class="login !rounded-[var(--rounded-big)]" :show-close="false" append-to-body>
<div class="relative">
<span class="iconfont icon-tubiaoV6-3 absolute top-[-33px] right-[-33px] text-[#fff] !text-[24px]" @click="handleClose"></span>
<login v-if="type === 'login' && dialogVisible" @typeChange="typeChange"/>
<register v-if="type === 'register' && dialogVisible" @typeChange="typeChange" />
</div>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref,computed,watch } from 'vue'
import login from './login.vue'
import register from './register.vue'
import useMemberStore from '@/stores/member'
const memberStore = useMemberStore()
//
const dialogVisible = computed(()=>{
return memberStore.loginPopup
})
//
const beforeClose = (next)=>{
memberStore.logClose()
type.value = 'login'
next()
}
const handleClose = ()=>{
memberStore.logClose()
type.value = 'login'
}
//
let type = ref('login')
const typeChange = (val:any)=>{
type.value = val
}
</script>
<style>
.login .el-dialog__header{
padding: 0 !important;
}
.login .el-dialog__body{
padding: 0 !important;
}
.login .el-dialog__headerbtn{
z-index: 99;
}
</style>
<style lang="scss" scoped>
:deep(.el-form-item) {
.el-input__wrapper {
box-shadow: unset !important;
border-radius: 0;
&.is-focus {
border: none;
}
}
&.is-error {
.el-input__wrapper {
border: none;
}
}
}
:deep(.el-form-item__error) {
padding-top: 5px;
}
.text-color {
color: var(--el-color-primary);
}
</style>

View File

@ -0,0 +1,278 @@
<template>
<div>
<span v-if="active" class="iconfont icon-mianxing_denglu_erweimadenglu !text-[#333] !text-[50px] absolute top-0 right-0 cursor-pointer" @click="handleChange"></span>
<span v-else class="iconfont icon-zhanghaodenglu !text-[#333] !text-[50px] absolute top-0 right-0 cursor-pointer" @click="handleChange"></span>
<div v-if="active" class="bg-white w-full py-[60px] px-[30px] !rounded-[var(--rounded-big)]">
<div class="flex items-end justify-center mb-[30px]">
<div class="text-[18px] cursor-pointer text-[#999] leading-[24px] oppoSans-R" :class="{ '!text-[#333] font-600': type == item.type,'mr-[70px]': (index+1) != loginType.length }" v-for="(item,index) in loginType" @click="type = item.type">{{item.title }}</div>
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" :validate-on-rule-change="false">
<div v-show="type == 'username'">
<el-form-item prop="username">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.username" :placeholder="t('usernamePlaceholder')" clearable :inline-message="true" :readonly="real_name_input" @click="real_name_input = false" @blur="real_name_input = true">
<template #prefix>
<span class="iconfont icon-woV6xx1 !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="password">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.password" :placeholder="t('passwordPlaceholder')" type="password" clearable show-password >
<template #prefix>
<span class="iconfont icon-mima !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
</div>
<div v-show="type == 'mobile'">
<el-form-item prop="mobile">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.mobile" :placeholder="t('mobilePlaceholder')" clearable>
<template #prefix>
<span class="iconfont icon-shoujiV6xx !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="mobile_code">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.mobile_code" :placeholder="t('codePlaceholder')">
<template #prefix>
<span class="iconfont icon-a-zhibao5 !mr-[14px]"></span>
</template>
<template #suffix>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key" @click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</div>
</el-form-item>
</div>
<div class="flex justify-between">
<el-button type="primary" link @click="typeChange" class="!text-[12px]">{{ t('noAccount') }}{{ t('toRegister') }}</el-button>
</div>
<div class="mt-[20px]">
<el-button type="primary" class="w-full !h-[50px] !rounded-[8px] oppoSans-M" size="large" @click="handleLogin" :loading="loading">{{ loading ? t('logining') : t('login') }}</el-button>
</div>
<div class="text-[12px] leading-[24px] flex items-center w-full mt-[20px]" v-if="configStore.login.agreement_show">
<span class="iconfont text-primary mr-[5px]" :class="isAgree ? 'icon-xuanze1' : 'icon-checkbox_nol'" @click="isAgree = !isAgree"></span>
{{ t('agreeTips') }}
<NuxtLink :to="service" target="_blank">
<span class="text-primary mx-[4px]">{{ t('userAgreement') }}</span>
</NuxtLink>
{{ t('and') }}
<NuxtLink :to="privacy" target="_blank">
<span class="text-primary mx-[4px]">{{ t('privacyAgreement') }}</span>
</NuxtLink>
</div>
</el-form>
</div>
<div v-else class="flex flex-col items-center py-[60px] px-[30px]">
<div class="text-[18px] cursor-pointer text-[#999] leading-[24px] oppoSans-R !text-[#333] font-600">微信扫码登录</div>
<div class="qrcode p-[20px] mt-[30px] border leading-none box-content rounded-[var(--rounded-small)]">
<div class="relative">
<el-image v-if="weixinCode.url" :src="weixinCode.url" class="w-[200px] h-[200px]"/>
<div v-else class="w-[202px] h-[202px]"></div>
<div class="flex flex-col justify-center items-center absolute inset-0 bg-gray-50" v-if="weixinCode.pastDue">
<span class="text-xs text-gray-600">{{ weixinCode.pastDueContent }}</span>
<span @click="scanLoginFn()" class="text-xs cursor-pointer text-color mt-2">点击刷新</span>
</div>
</div>
<div class="mt-[22px] flex items-center justify-center">
<span class="iconfont icon-weixin1 text-[#00c22c]"></span>
<span class="text-[14px] text-[#999] ml-[4px]">微信扫一扫</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref,reactive,computed,onUnmounted } from 'vue'
import { FormInstance } from 'element-plus'
import { usernameLogin, mobileLogin, scanlogin, checkscan } from '@/app/api/auth'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import QRCode from "qrcode";
const memberStore = useMemberStore()
const configStore = useConfigStore()
//
const service = ref('')
const privacy = ref('')
if(location.pathname.indexOf('web') != -1){
service.value = '/web/auth/agreement?key=service'
privacy.value = '/web/auth/agreement?key=privacy'
}else{
service.value = '/auth/agreement?key=service'
privacy.value = '/auth/agreement?key=privacy'
}
//
let active = ref(true)
let timer:any = null
const handleChange = () => {
active.value = !active.value
if(!active.value){
scanLoginFn();
}else{
clearTimeout(timer)
}
}
onUnmounted(() => {
clearTimeout(timer)
});
//
const checkScanFn = (key) => {
let parameter = { key };
checkscan(parameter).then((res) => {
let data = res.data;
switch (data.status) {
case 'wait':
timer = setTimeout(() => {
checkScanFn(weixinCode.value.key);
}, 1000);
break;
case 'success':
if (!data.login_data.token) {
useCookie('openId').value = data.login_data.openid
navigateTo(`/auth/bind`)
} else {
memberStore.setToken(data.login_data.token)
memberStore.logClose()
}
break;
case 'fail':
weixinCode.value.pastDueContent = data.fail_reason
weixinCode.value.pastDue = true;
break;
}
}).catch((res) => {
weixinCode.value.pastDue = true;
weixinCode.value.pastDueContent = res.msg;
})
}
// ,
const weixinCode = ref({
url: '',
key: '',
pastDue: false,
pastDueContent: '二维码生成失败'
})
const scanLoginFn = async () => {
let data = await (await scanlogin()).data;
weixinCode.value.key = data.key
if(data.url) {
QRCode.toDataURL(data.url, { errorCorrectionLevel: 'L', margin: 0, width: 100 }).then(url => {
weixinCode.value.url = url
});
weixinCode.value.pastDue = false;
setTimeout(() => {
checkScanFn(weixinCode.value.key);
}, 1000);
}
}
configStore.getLoginConfig()
const loginType = computed(() => {
const value = []
configStore.login.is_username && (value.push({ type: 'username', title: t('usernameLogin') }))
configStore.login.is_mobile && (value.push({ type: 'mobile', title: t('mobileLogin') }))
type.value = value[0] ? value[0].type : ''
return value
})
const loading = ref(false)
const type = ref('')
const formData = reactive({
username: '',
password: '',
mobile: '',
mobile_code: '',
mobile_key: ''
})
const formRef = ref<FormInstance>()
const formRules = computed(() => {
return {
'username': {
required: type.value == 'username',
message: t('usernamePlaceholder'),
trigger: ['blur', 'change'],
},
'password': {
required: type.value == 'username',
message: t('passwordPlaceholder'),
trigger: ['blur', 'change']
},
'mobile': [
{
required: type.value == 'mobile',
message: t('mobilePlaceholder'),
trigger: ['blur', 'change'],
},
{
validator(rule: any, value: string, callback: any) {
if (type.value != 'mobile') return true
else return test.mobile(value)
},
message: t('mobileError'),
trigger: ['blur'],
}
],
'mobile_code': {
required: type.value == 'mobile',
message: t('codePlaceholder'),
trigger: ['change']
}
}
})
const isAgree = ref(false)
const handleLogin = async () => {
await formRef.value?.validate(async (valid, fields) => {
if (valid) {
if (configStore.login.agreement_show && !isAgree.value) {
ElMessage.error(t('isAgreeTips'))
return false;
}
if (loading.value) return
loading.value = true
const login = type.value == 'username' ? usernameLogin : mobileLogin
login(formData).then(async (res) => {
await memberStore.setToken(res.data.token)
memberStore.logClose()
}).catch(() => {
loading.value = false
})
}
})
}
const smsCodeRef = ref<AnyObject | null>(null)
const sendSmsCode = async () => {
await formRef.value?.validateField('mobile', async (valid, fields) => {
if (valid) {
smsCodeRef.value?.send()
}
})
}
//
const emit = defineEmits(['typeChange'])
const typeChange = ()=>{
emit('typeChange','register')
}
const real_name_input = ref(true)
const password_input = ref(true)
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,271 @@
<template>
<div>
<div class="bg-white w-full py-[60px] px-[30px] !rounded-[var(--rounded-big)]">
<div class="flex items-end justify-center mb-[30px]">
<div class="text-[18px] cursor-pointer text-[#999] leading-[24px] oppoSans-R" :class="{ '!text-[#333] font-600': type == item.type,'mr-[70px]': (index+1) != registerType.length }" v-for="(item,index) in registerType" @click="type = item.type">{{item.title }}</div>
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" :validate-on-rule-change="false">
<div v-show="type == 'username'">
<el-form-item prop="username">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.username" :placeholder="t('usernamePlaceholder')" clearable :inline-message="true" :readonly="real_name_input" @click="real_name_input = false" @blur="real_name_input = true">
<template #prefix>
<span class="iconfont icon-woV6xx1 !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="password">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.password" :placeholder="t('passwordPlaceholder')" type="password" clearable :show-password="true" >
<template #prefix>
<span class="iconfont icon-mima !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="confirm_password">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" type="password" clearable :show-password="true" >
<template #prefix>
<span class="iconfont icon-mima !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
</div>
<div v-show="type == 'mobile' || configStore.login.is_bind_mobile">
<el-form-item prop="mobile">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.mobile" :placeholder="t('mobilePlaceholder')" clearable>
<template #prefix>
<span class="iconfont icon-shoujiV6xx !mr-[14px]"></span>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="mobile_code">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.mobile_code" :placeholder="t('codePlaceholder')">
<template #prefix>
<span class="iconfont icon-a-zhibao5 !mr-[14px]"></span>
</template>
<template #suffix>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key" @click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</div>
</el-form-item>
</div>
<div v-show="type == 'username'">
<el-form-item prop="captcha_code">
<div class="flex-1 h-[50px] border-[1px] border-solid border-[#ccc] rounded-[8px] flex items-center">
<el-input v-model="formData.captcha_code" :placeholder="t('captchaPlaceholder')">
<template #prefix>
<span class="iconfont icon-a-zhibao5 !mr-[14px]"></span>
</template>
<template #suffix>
<div class="py-0 leading-none">
<el-image :src="captcha.image.value" class="h-[30px] cursor-pointer" @click="captcha.refresh()"></el-image>
</div>
</template>
</el-input>
</div>
</el-form-item>
</div>
<div class="flex justify-end">
<el-button type="primary" link @click="typeChange" class="!text-[12px]">{{ t('haveAccount') }}{{ t('toLogin') }}</el-button>
</div>
<div class="mt-[20px]">
<el-button type="primary" class="w-full !h-[50px] !rounded-[8px] oppoSans-M" size="large" @click="handleRegister" :loading="loading">{{ loading ? t('registering') : t('register') }}</el-button>
</div>
<div class="text-[12px] leading-[24px] flex items-center w-full mt-[20px]" v-if="configStore.login.agreement_show">
<span class="iconfont text-primary mr-[5px]" :class="isAgree ? 'icon-xuanze1' : 'icon-checkbox_nol'" @click="isAgree = !isAgree"></span>
{{ t('registerAgreeTips') }}
<NuxtLink :to="service" target="_blank">
<span class="text-primary mx-[4px]">{{ t('userAgreement') }}</span>
</NuxtLink>
{{ t('and') }}
<NuxtLink :to="privacy" target="_blank">
<span class="text-primary mx-[4px]">{{ t('privacyAgreement') }}</span>
</NuxtLink>
</div>
</el-form>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref,reactive,computed } from 'vue'
import { usernameRegister, mobileRegister, wechatCheck } from '@/app/api/auth'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import { FormInstance } from 'element-plus'
definePageMeta({
layout: "container"
});
const memberStore = useMemberStore()
const configStore = useConfigStore()
configStore.getLoginConfig()
//
const service = ref('')
const privacy = ref('')
if(location.pathname.indexOf('web') != -1){
service.value = '/web/auth/agreement?key=service'
privacy.value = '/web/auth/agreement?key=privacy'
}else{
service.value = '/auth/agreement?key=service'
privacy.value = '/auth/agreement?key=privacy'
}
const type = ref('')
const registerType = computed(() => {
const value = []
configStore.login.is_username && (value.push({ type: 'username', title: t('usernameRegister') }))
configStore.login.is_mobile && !configStore.login.is_bind_mobile && (value.push({ type: 'mobile', title: t('mobileRegister') }))
type.value = value[0] ? value[0].type : ''
return value
})
const loading = ref(false)
const formData = reactive({
username: '',
password: '',
confirm_password: '',
mobile: '',
mobile_code: '',
mobile_key: '',
captcha_key: '',
captcha_code: ''
})
const formRules = computed(() => {
return {
'username': {
type: 'string',
required: type.value == 'username',
message: t('usernamePlaceholder'),
trigger: ['blur', 'change'],
},
'password': {
type: 'string',
required: type.value == 'username',
message: t('passwordPlaceholder'),
trigger: ['blur', 'change']
},
'confirm_password': [
{
type: 'string',
required: type.value == 'username',
message: t('confirmPasswordPlaceholder'),
trigger: ['blur', 'change']
},
{
validator(rule: any, value: string, callback: any) {
return value == formData.password
},
message: t('confirmPasswordError'),
trigger: ['change', 'blur'],
}
],
'mobile': [
{
type: 'string',
required: type.value == 'mobile' || configStore.login.is_bind_mobile,
message: t('mobilePlaceholder'),
trigger: ['blur', 'change'],
},
{
validator(rule: any, value: string, callback: any) {
if (type.value != 'mobile' && !configStore.login.is_bind_mobile) return true
else return test.mobile(value)
},
message: t('mobileError'),
trigger: ['change', 'blur'],
}
],
'mobile_code': {
type: 'string',
required: type.value == 'mobile' || configStore.login.is_bind_mobile,
message: t('codePlaceholder'),
trigger: ['blur', 'change']
},
'captcha_code': {
type: 'string',
required: type.value == 'username',
message: t('captchaPlaceholder'),
trigger: ['blur', 'change'],
}
}
})
const isAgree = ref(false)
const formRef = ref<FormInstance>()
const handleRegister = async () => {
await formRef.value?.validate(async (valid, fields) => {
if (valid) {
if (configStore.login.agreement_show && !isAgree.value) {
ElMessage.error(t('isAgreeTips'))
return false;
}
if (loading.value) return
loading.value = true
const register = type.value == 'username' ? usernameRegister : mobileRegister
register(formData).then((res: any) => {
memberStore.setToken(res.data.token)
memberStore.logClose()
}).catch(() => {
loading.value = false
captcha.refresh()
})
}
})
}
let show = ref(false)
const wechatCheckFn = () =>{
wechatCheck().then((res:any) =>{
show.value = res.data
})
}
wechatCheckFn()
//
const captcha = useCaptcha(formData)
captcha.refresh()
//
const smsCodeRef = ref<AnyObject | null>(null)
const sendSmsCode = async () => {
await formRef.value?.validateField('mobile', async (valid, fields) => {
if (valid) {
smsCodeRef.value?.send()
}
})
}
//
const emit = defineEmits(['typeChange'])
const typeChange = ()=>{
emit('typeChange','login')
}
const real_name_input = ref(true)
const password_input = ref(true)
const confirm_password_input = ref(true)
</script>
<style lang="scss" scoped>
:deep(.el-checkbox.el-checkbox--large){
height: 0px;
margin-right: 5px;
}
</style>

View File

@ -1,34 +1,64 @@
<template>
<el-menu
:default-active="appStore.route"
:ellipsis="false" :router="true"
class="el-menu-vertical-demo w-[200px]"
>
<el-menu-item index="/member" route="/member" class="divide-y">
<span>欢迎页</span>
</el-menu-item>
<el-menu-item index="/member/center" route="/member/center" class="divide-y">
<span>个人信息</span>
</el-menu-item>
<el-menu-item index="/member/balance" route="/member/balance" class="divide-y">
<span>我的余额</span>
</el-menu-item>
<el-menu-item index="/member/point" route="/member/point" class="divide-y">
<span>我的积分</span>
</el-menu-item>
</el-menu>
<div class="w-[180px] flex-shrink-0">
<div class="bg-[#fff] pt-[30px] pr-[30px] pb-[21px] pl-[45px] rounded-[var(--rounded-big)]">
<el-collapse v-model="activeNames">
<el-collapse-item name="1" class="!mb-[24px]">
<template #title>
<div class="flex items-center">
<span class="text-[16px]">账户设置</span>
</div>
</template>
<div class="text-[14px] leading-[24px] cursor-pointer text-[#666]" :class="{'!text-primary': appStore.route == '/app/member/center'}" @click="router.push('/app/member/center')">个人资料</div>
</el-collapse-item>
<el-collapse-item name="2" class="!mb-[24px]">
<template #title>
<div class="flex items-center">
<span class="text-[16px]">我的账户</span>
</div>
</template>
<div class="text-[14px] leading-[24px] cursor-pointer text-[#666] mb-[24px]" :class="{'!text-primary': appStore.route == '/app/member/point'}" @click="router.push('/app/member/point')">我的积分</div>
<div class="text-[14px] leading-[24px] cursor-pointer text-[#666]" :class="{'!text-primary': appStore.route == '/app/member/balance'}" @click="router.push('/app/member/balance')">我的余额</div>
</el-collapse-item>
<el-collapse-item name="7" class="!mb-[24px]">
<template #title>
<div class="flex items-center">
<span class="text-[16px] oppoSans-M">规则协议</span>
</div>
</template>
<div class="text-[14px] leading-[24px] cursor-pointer text-[#666] mb-[24px]" :class="{'!text-primary': appStore.route == '/app/auth/agreement' && route.query.key == 'service'}" @click="router.push({path:'/app/auth/agreement',query:{key:'service'}})">用户协议</div>
<div class="text-[14px] leading-[24px] cursor-pointer text-[#666]" :class="{'!text-primary': appStore.route == '/app/auth/agreement' && route.query.key == 'privacy'}" @click="router.push({path:'/app/auth/agreement',query:{key:'privacy'}})">隐私协议</div>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import useAppStore from '@/stores/app'
import { useRouter, useRoute} from 'vue-router'
const appStore = useAppStore()
const router = useRouter()
const route = useRoute()
const activeNames = ref(['1','2','3','4','5','6','7'])
</script>
<style lang="scss" scoped>
.el-menu-vertical-demo{
border: none !important;
.el-menu-item{
border-bottom: 1px solid #F1F1F1;
}
:deep(.el-collapse){
border:none !important;
}
:deep(.el-collapse-item__wrap){
border:none !important;
}
:deep(.el-collapse-item__header){
--el-collapse-header-height: auto !important;
--el-collapse-header-font-size: 16px;
border:none !important;
margin-bottom: 24px;
}
:deep(.el-collapse-item__content){
padding-bottom:0 !important;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="h-[30px]">
<el-button type="primary" link :disabled="!sendSms.canGetCode.value" @click="handleClick">{{ sendSms.text.value }}</el-button>
<div class="h-[30px] leading-[28px]">
<el-button type="primary" link class="!text-[12px]" :disabled="!sendSms.canGetCode.value" @click="handleClick">{{ sendSms.text.value }}</el-button>
</div>
<el-dialog v-model="captchaDialog" :title="t('captchaTitle')" width="350px" :append-to-body="true" :align-center="true">
@ -59,7 +59,7 @@ const formRules = reactive({
captcha_code: {
required: true,
message: t('captchaPlaceholder'),
trigger: ['blur', 'change']
trigger: ['blur']
}
})
const formRef = ref<AnyObject | null>(null)
@ -86,6 +86,8 @@ const confirm = async () => {
if (sendRes) {
value.value = sendRes
captchaDialog.value = false
captcha.refresh()
formData.captcha_code = ''
loading.value = false
} else if (sendRes === false) {
captcha.refresh()

View File

@ -0,0 +1,184 @@
<template>
<div class="flex flex-wrap">
<template v-if="limit == 1">
<div class="rounded cursor-pointer overflow-hidden relative border border-solid border-color mr-[10px]" :style="style">
<div class="w-full h-full relative image-wrap" v-if="images.data.length">
<div class="w-full h-full flex items-center justify-center">
<el-image :src="img(images.data[0])" fit="contain"></el-image>
</div>
<div class="absolute z-[1] inset-0 image-mask hidden">
<div class="flex items-center justify-center w-full h-full bg-black bg-opacity-60 operation">
<icon name="element-ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage()" />
<icon name="element-Delete" color="#fff" size="18px" v-if="status" @click="removeImage" />
</div>
</div>
</div>
<el-upload v-bind="upload" class="upload-file w-full h-full" :show-file-list="false" >
<div class="w-full h-full flex items-center justify-center flex-col">
<icon name="element-Plus" size="20px" color="var(--el-text-color-secondary)" />
<div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText }}</div>
</div>
</el-upload>
</div>
</template>
<template v-else>
<div class="rounded cursor-pointer overflow-hidden relative border border-solid border-color mr-[10px] mb-[10px]" :style="style" v-for="(item, index) in images.data" :key="index">
<div class="w-full h-full relative image-wrap">
<div class="w-full h-full flex items-center justify-center">
<el-image :src="img(item)" fit="contain"></el-image>
</div>
<div class="absolute z-[1] inset-0 image-mask hidden">
<div class=" flex items-center justify-center w-full h-full bg-black bg-opacity-60 operation">
<icon name="element-ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(index)" />
<icon name="element-Delete" color="#fff" size="18px" v-if="status" @click="removeImage(index)" />
</div>
</div>
</div>
</div>
<div class="rounded-[6px] cursor-pointer overflow-hidden relative border border-dashed border-color bg-[#fafafa] hover:border-primary" :style="style" v-if="images.data.length < limit && status">
<el-upload v-bind="upload" class="upload-file w-full h-full" :show-file-list="false" :multiple="true" :limit="limit">
<div class="w-full h-full flex items-center justify-center">
<icon name="element-Plus" size="28px" color="var(--el-text-color-secondary)" />
<div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText }}</div>
</div>
</el-upload>
</div>
</template>
</div>
<el-image-viewer :url-list="previewImageList" v-if="imageViewer.show" @close="imageViewer.show = false"
:initial-index="imageViewer.index" :zoom-rate="1" :hide-on-click-modal="true" />
</template>
<script lang="ts" setup>
import { reactive,computed,watch,toRaw } from 'vue'
import { getToken,img } from '@/utils/common'
import { UploadFile, ElMessage, UploadFiles } from 'element-plus'
const prop = defineProps({
modelValue: {
type: String,
default: ''
},
data: {
type: Array,
default: []
},
width: {
type: String,
default: '100px'
},
height: {
type: String,
default: '100px'
},
//
imageText: {
type: String
},
//
limit: {
type: Number,
default: 1
},
//
status:{
type:Boolean,
default:true
}
})
const emit = defineEmits(['update:modelValue','success'])
const value = computed({
get() {
return prop.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const images: Record<string, any> = reactive({
data: []
})
let previewImageList: string[] = reactive([])
const setValue = () => {
value.value = toRaw(images.data).toString()
previewImageList = toRaw(images.data).map((url: string) => { return img(url) })
}
watch(() => value.value, () => {
images.data = [
...value.value.split(',').filter((item: string) => { return item })
]
setValue()
}, { immediate: true })
const style = computed(() => {
return {
width: prop.width,
height: prop.height
}
})
const headers: Record<string, any> = {}
headers.token = getToken()
const runtimeConfig = useRuntimeConfig()
let url = runtimeConfig.public.VITE_APP_BASE_URL || `${location.origin}/api/`
const upload: Record<string, any> = {
action: `${url}/file/image`,
headers,
accept: '.png,.jpg,.jpeg',
//
beforeUpload: (file: File) => {
if (images.data.length >= prop.limit) {
ElMessage.error(`最多只能上传 ${prop.limit} 张图片`);
return false;
}
return true; //
},
onSuccess: (response: any) => {
images.data.push(response.data.url);
setValue();
},
onExceed: () => {
ElMessage.error(`最多只能上传 ${prop.limit} 张图片`);
},
};
/**
* 删除图片
* @param index
*/
const removeImage = (index: number = 0) => {
images.data.splice(index, 1)
setValue()
}
/**
* 查看图片
*/
const imageViewer = reactive({
show: false,
index: 0
})
const previewImage = (index: number = 0) => {
imageViewer.show = true
imageViewer.index = index
}
</script>
<style lang="scss">
.upload-file .el-upload {
width: 100%;
height: 100%;
}
.image-wrap:hover .image-mask {
display: block;
}
</style>

View File

@ -1,3 +1,4 @@
import { ref } from 'vue'
import { getCaptcha } from '@/app/api/system'
interface formData {

View File

@ -1,4 +1,5 @@
import type { LocationQueryRaw } from 'vue-router'
import storage from '@/utils/storage'
export function useLogin() {
/**
@ -17,17 +18,12 @@ export function useLogin() {
/**
*
*/
const handleLoginBack = () => {
const data = storage.get('loginBack')
if (data) {
useRouter().push({ path: data.path, query: data.query })
} else {
useRouter().push({ path: '/' })
}
const handleLoginBack = (callbak:any) => {
if(callbak) callbak()
}
return {
setLoginBack,
handleLoginBack
}
}
}

View File

@ -1,3 +1,4 @@
import { reactive, ref } from 'vue'
import { sendSms } from '@/app/api/system'
export function useSendSms() {

View File

@ -4,6 +4,25 @@
"getSmsCode": "获取短信验证码",
"smsCodeChangeText": "秒后重新获取",
"captchaTitle": "请先完成安全验证",
"logining": "登录中",
"usernamePlaceholder": "请输入账号",
"passwordPlaceholder": "请输入密码",
"resetpwd": "忘记密码",
"noAccount": "还没有账号",
"toRegister": "去注册",
"registering": "注册中",
"confirmPasswordPlaceholder": "请再次确认密码",
"confirmPasswordError": "两次输入的密码不一致",
"haveAccount": "已有账号",
"toLogin": "去登录",
"and": "和",
"registerAgreeTips": "注册代表您同意",
"usernameRegister": "账号注册",
"mobileRegister": "手机号注册",
"agreeTips": "请阅读并同意",
"isAgreeTips":"请先阅读并同意协议",
"usernameLogin": "密码登录",
"mobileLogin": "验证码登录",
"confirm": "确认",
"cancel": "取消",
"captchaPlaceholder": "请输入验证码",
@ -14,7 +33,6 @@
"privacyAgreement": "隐私协议",
"protocolNotConfigured": "未配置协议",
"siteClose": "站点已关闭",
"noSite": "站点不存在",
"request": {
"unknownError": "未知错误",
"400": "错误的请求",

View File

@ -0,0 +1,22 @@
<template>
<div class="ml-[20px] min-h-[70vh] w-[1000px] flex">
<div class="m-auto">
<div class="text-[#333] text-center text-[24px] mt-[35px] mb-[26px] ">请登录查看</div>
<div class="w-[100px] h-[40px] leading-[40px] border-[1px] border-solid border-[#ccc] rounded-full text-center text-[14px] mx-auto cursor-pointer" @click="handleLogin">登录</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, computed,watch} from 'vue'
import useMemberStore from '@/stores/member'
const memberStore = useMemberStore()
const handleLogin = ()=>{
memberStore.logOpen()
}
</script>
<style scoped>
</style>

View File

@ -1,29 +1,14 @@
<template>
<div class="flex h-[220px] min-w-[1200px] bg-[#3F4045]">
<div class="mt-[70px] w-full">
<p class="text-center text-[#999]">
<p class="text-center text-[#999]" v-if="friendlyLink.length">
<span>友情链接</span>
<NuxtLink to="https://www.bt.cn">
<span class="mr-[10px]">宝塔</span>|
</NuxtLink>
<NuxtLink to="https://www.oschina.net">
<span class="mr-[10px]">开源中国</span>|
</NuxtLink>
<NuxtLink to="https://www.aliyun.com">
<span class="mr-[10px]">阿里云</span>|
</NuxtLink>
<NuxtLink to="https://gitee.com/">
<span class="mr-[10px]">码云Gitee</span>|
</NuxtLink>
<NuxtLink to="https://cloud.tencent.com/">
<span class="mr-[10px]">腾讯云</span>|
</NuxtLink>
<NuxtLink to="https://mp.weixin.qq.com">
<span class="mr-[10px]">微信公众平台</span>|
</NuxtLink>
<NuxtLink to="http://www.thinkphp.cn">
<span class="mr-[10px]">Thinkphp</span>
</NuxtLink>
<template v-for="(item,index) in friendlyLink" :key="index">
<NuxtLink :to="item.link_url" target="_blank">
<span>{{item.link_title}}</span>
<span class="mx-[10px] text-[#D9D9D9]" v-if="(index + 1) != friendlyLink.length">|</span>
</NuxtLink>
</template>
</p>
<p class="text-center mt-[20px] text-[#999]" v-if="copyright">
<NuxtLink :to="copyright.gov_url" v-if="copyright.gov_record">
@ -44,6 +29,7 @@
<script lang="ts" setup>
import { getCopyRight } from '@/app/api/system';
import { reactive, ref } from 'vue'
import { getFriendlyLink } from '@/app/api/system'
const copyright = ref(null);
const getCopy = () => {
@ -52,6 +38,15 @@ const getCopy = () => {
})
}
getCopy()
const friendlyLink = ref([]) // { link_title: '', link_url: '' }
const getFriendlyLinkFn = () =>{
getFriendlyLink().then((res:any) =>{
friendlyLink.value = res.data
})
}
getFriendlyLinkFn()
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,5 @@
<template>
<div class="flex h-full min-w-[1200px]">
<div class="flex h-full min-w-[1200px] bg-[#fff]">
<div class="flex items-center ml-[20px]">
<NuxtLink to="/">
<div class="w-[132px] mr-[10px]"><img src="@/assets/images/index/logo.jpg" /></div>
@ -10,48 +10,57 @@
<div class="mx-auto flex-shrink">
<el-menu :default-active="appStore.route" class="h-full" mode="horizontal" :ellipsis="false" :router="true">
<el-menu-item index="/" route="/">
<el-menu-item index="/" route="/app/index">
<span class="text-base mx-4">首页</span>
<span></span>
</el-menu-item>
<el-menu-item index="/article/list" route="/article/list">
<span class="text-base mx-4">文章</span>
<span></span>
</el-menu-item>
<el-menu-item route="/">
<span class="text-base mx-4">社区</span>
<el-menu-item index="bbs">
<span class="text-base mx-4" @click.stop="openBbs">社区</span>
<span></span>
</el-menu-item>
</el-menu>
</div>
<div class="flex items-center justify-end mr-[20px] ml-auto whitespace-pre">
<div class="flex items-center justify-end mr-[20px] ml-auto whitespace-pre-wrap">
<div v-if="info">
<NuxtLink to="/member/center">
<NuxtLink to="/app/member/center">
<span class="cursor-pointer">{{ info.nickname }}</span>
</NuxtLink>
<span class="mx-2">|</span>
<span class="cursor-pointer" @click="logoutFn">退出</span>
</div>
<NuxtLink to="/auth/login" v-else>
<el-button type="primary" link>{{ t('login') }} / {{ t('register') }}</el-button>
</NuxtLink>
<el-button type="primary" link v-else @click="toLogin">{{ t('login') }} / {{ t('register') }}</el-button>
</div>
<LoadingDialog/>
</div>
</template>
<script lang="ts" setup>
import { getToken } from '@/utils/common'
import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
import useConfigStore from '@/stores/config'
import LoadingDialog from '@/components/login-dialog/index.vue'
const configStore = useConfigStore()
const memberStore = useMemberStore()
const info = computed(() => memberStore.info)
const toLogin = () => {
if(!getToken() && !configStore.login.is_username && !configStore.login.is_mobile && !configStore.login.is_bind_mobile){
ElMessage.error('商家未开启普通账号登录注册')
return false
}
memberStore.logOpen()
}
const logoutFn = () => {
memberStore.logout()
navigateTo(`/auth/login`)
navigateTo(`/app/index`)
}
const openBbs = () => {
window.open('https://www.niushop.com/bbs.html')
}
const appStore = useAppStore()
</script>

44
web/layouts/member.vue Normal file
View File

@ -0,0 +1,44 @@
<template>
<el-container class="w-screen h-screen">
<el-header>
<layout-header />
</el-header>
<el-main class="p-0 min-w-[1200px]">
<div class="bg-page pt-6 pb-6">
<div class="main-container flex justify-between">
<sidebar></sidebar>
<div v-if="agreeShow"><slot></slot></div>
<div v-else><layout-error ></layout-error></div>
</div>
</div>
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import layoutHeader from './default/components/header/index.vue'
import layoutError from './default/components/error/index.vue'
import sidebar from '@/components/sidebar/index.vue'
import { getToken } from '@/utils/common'
import { useRouter } from 'vue-router'
const router = useRouter()
let agreeShow = ref(false)
watch(()=> router.currentRoute.value.path ,(newValue)=>{
if(router.currentRoute.value.path == '/auth/agreement' || router.currentRoute.value.path == '/app/auth/agreement' || getToken() ){
agreeShow.value = true
}else{
agreeShow.value = false
}
},{immediate:true,deep: true})
</script>
<style lang="scss" scoped>
.el-header {
--el-header-padding: 0;
}
.el-main {
--el-main-padding: 0;
}
</style>

View File

@ -1,6 +1,12 @@
import useAppStore from '@/stores/app'
export default defineNuxtRouteMiddleware((to, from) => {
if (!getToken()) {
useLogin().setLoginBack(to)
return navigateTo('/auth/login')
useAppStore().$patch(state => {
state.route = to.path
})
// useLogin().setLoginBack(to)
// return navigateTo('/auth/login')
}
})

View File

@ -24,7 +24,7 @@ export default defineNuxtConfig({
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: i => `__tla_${i}`
})
}),
]
},
ssr: false

54
web/package-lock.json generated
View File

@ -10,10 +10,13 @@
"hasInstallScript": true,
"dependencies": {
"@vueuse/core": "^9.13.0",
"aos": "^2.3.4",
"element-plus": "^2.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
"sass": "^1.60.0"
"qs": "6.7.0",
"sass": "^1.60.0",
"swiper": "^11.1.15"
},
"devDependencies": {
"@element-plus/nuxt": "^1.0.4",
@ -2389,6 +2392,16 @@
"node": ">= 8"
}
},
"node_modules/aos": {
"version": "2.3.4",
"resolved": "https://registry.npmmirror.com/aos/-/aos-2.3.4.tgz",
"integrity": "sha512-zh/ahtR2yME4I51z8IttIt4lC1Nw0ktsFtmeDzID1m9naJnWXhCoARaCgNOGXb5CLy3zm+wqmRAEgMYB5E2HUw==",
"dependencies": {
"classlist-polyfill": "^1.0.3",
"lodash.debounce": "^4.0.6",
"lodash.throttle": "^4.0.1"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.0.0.tgz",
@ -2829,6 +2842,11 @@
"node": ">=8"
}
},
"node_modules/classlist-polyfill": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz",
"integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ=="
},
"node_modules/clear": {
"version": "0.1.0",
"resolved": "https://registry.npmmirror.com/clear/-/clear-0.1.0.tgz",
@ -4962,8 +4980,7 @@
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"dev": true
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
@ -5026,6 +5043,11 @@
"lodash._reinterpolate": "^3.0.0"
}
},
"node_modules/lodash.throttle": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz",
@ -6887,6 +6909,14 @@
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -7600,6 +7630,24 @@
"node": ">=14.0.0"
}
},
"node_modules/swiper": {
"version": "11.1.15",
"resolved": "https://registry.npmmirror.com/swiper/-/swiper-11.1.15.tgz",
"integrity": "sha512-IzWeU34WwC7gbhjKsjkImTuCRf+lRbO6cnxMGs88iVNKDwV+xQpBCJxZ4bNH6gSrIbbyVJ1kuGzo3JTtz//CBw==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.2.1.tgz",

View File

@ -15,14 +15,17 @@
"nuxt": "^3.4.1",
"nuxt-windicss": "^2.6.0",
"sass": "^1.60.0",
"vue-i18n": "^9.2.2",
"vite-plugin-top-level-await": "^1.3.1"
"vite-plugin-top-level-await": "^1.3.1",
"vue-i18n": "^9.2.2"
},
"dependencies": {
"@vueuse/core": "^9.13.0",
"aos": "^2.3.4",
"element-plus": "^2.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
"sass": "^1.60.0"
"sass": "^1.60.0",
"swiper": "^11.1.15",
"qs": "6.7.0"
}
}
}

6
web/plugins/aos.ts Normal file
View File

@ -0,0 +1,6 @@
import AOS from 'aos';
import "aos/dist/aos.css";
export default defineNuxtPlugin((NuxtApp) => {
AOS.init(); // 初始化
NuxtApp.vueApp.use(AOS)
})

View File

@ -9,12 +9,10 @@ interface loginConfig {
agreement_show: number | boolean
}
interface Config {
login: loginConfig
}
const useConfigStore = defineStore('config', {
state: (): Config => {
return {
@ -28,15 +26,17 @@ const useConfigStore = defineStore('config', {
}
},
actions: {
async getLoginConfig() {
async getLoginConfig(router: any = null) {
await getConfig().then(({ data }) => {
this.login.is_username = parseInt(data.is_username)
this.login.is_mobile = parseInt(data.is_mobile)
this.login.is_auth_register = parseInt(data.is_auth_register)
this.login.is_bind_mobile = parseInt(data.is_bind_mobile)
this.login.agreement_show = parseInt(data.agreement_show)
if(data && router && router.currentRoute.value.path === '/site/close'){
navigateTo('/', { replace: true })
}
}).catch(() => {
})
}
}

View File

@ -4,14 +4,16 @@ import { logout } from '@/app/api/auth'
interface Member {
token: string | null
info: Record<string, any> | null
info: Record<string, any> | null,
loginPopup:boolean
}
const useMemberStore = defineStore('member', {
state: (): Member => {
return {
token: useCookie('token').value,
info: null
info: null,
loginPopup:false
}
},
actions: {
@ -36,6 +38,12 @@ const useMemberStore = defineStore('member', {
this.info = null
useCookie('token').value = null
logout().then().catch()
},
logOpen(){
this.loginPopup = true
},
logClose(){
this.loginPopup = false
}
}
})

View File

@ -5,6 +5,7 @@ import { getSiteInfo } from '@/app/api/system'
interface System {
lang: string,
site: Record<string, any>
}
const useSystemStore = defineStore('system', {
@ -18,15 +19,16 @@ const useSystemStore = defineStore('system', {
}
},
actions: {
async getSitenfo() {
await getSiteInfo()
.then((res: any) => {
this.site = res.data
if (this.site.status == 3) navigateTo('/site/close', { replace: true })
})
.catch((err) => {
navigateTo('/site/nosite', { replace: true })
})
async getSiteInfoFn() {
await getSiteInfo().then((res: any) => {
this.site = res.data
if (!('shop_web' in this.site.site_addons)) {
navigateTo('/app/index', { replace: true })
}
}).catch((err) => {
})
}
}
})

View File

@ -1,4 +1,5 @@
import useMemberStores from '@/stores/member'
import { ElMessage } from 'element-plus'
/**
* token
@ -7,11 +8,29 @@ import useMemberStores from '@/stores/member'
export function getToken(): null | string {
return useMemberStores().token
}
/**
*
* @param fn
* @param delay
* @returns
*/
export function debounce(fn: (args?: any) => any, delay: number = 300) {
let timer: null | number = null
return function (...args: any) {
if (timer != null) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay);
}
}
/**
* url
* @param str
* @returns
* @param str
* @returns
*/
export function isUrl(str: string): boolean {
return str.indexOf('http://') != -1 || str.indexOf('https://') != -1
@ -19,10 +38,75 @@ export function isUrl(str: string): boolean {
/**
*
* @param path
* @returns
* @param path
* @returns
*/
export function img(path: string): string {
const runtimeConfig = useRuntimeConfig()
return isUrl(path) ? path : `${runtimeConfig.public.VITE_IMG_DOMAIN || location.origin}/${path}`
}
}
/**
*
*/
export function moneyFormat(money: string): string {
return isNaN(parseFloat(money)) ? money : parseFloat(money).toFixed(2)
}
/**
* @description
* @param {object} obj
* @returns {*}
*/
export function deepClone(obj: object) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj
if (typeof obj !== 'object' && typeof obj !== 'function') {
// 原始类型直接返回
return obj
}
const o = isArray(obj) ? [] : {}
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i]
}
}
return o
}
const isArray = (value: any) => {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
}
return Object.prototype.toString.call(value) === '[object Array]'
}
/**
*
* @param {Object} value
* @param {Object} callback
*/
export function copy(value, callback) {
var oInput = document.createElement('input'); //创建一个隐藏input重要
oInput.value = value; //赋值
oInput.setAttribute("readonly", "readonly");
document.body.appendChild(oInput);
oInput.select(); // 选择对象
document.execCommand("Copy"); // 执行浏览器复制命令
oInput.className = 'oInput';
oInput.style.display = 'none';
document.body.removeChild(oInput); // 删除创建的对象
ElMessage({
message: '复制成功',
type: 'success',
})
typeof callback == 'function' && callback();
}
/**
*
* @param event
*/
export function filterSpecial(event:any){
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g,'')
}

View File

@ -63,7 +63,6 @@ class Language {
}
/**
*
* @param app
* @param path
*/

View File

@ -1,6 +1,6 @@
import { breakpointsTailwind } from '@vueuse/core'
import { ElMessage } from 'element-plus'
import useMemberStore from '@/stores/member'
import qs from 'qs'
interface ConfigOption {
showErrorMessage?: boolean
@ -48,7 +48,8 @@ class Http {
if (data.code == 1) {
if (options.showSuccessMessage) ElMessage({ message: data.msg, type: 'success' })
} else {
if (data.code == 0) {
if (options.showErrorMessage === false) return;
if (data.code == 0 || data.code == 400) {
ElMessage({ message: data.msg, type: 'error' })
} else {
this.handleAuthError(data.code)
@ -59,7 +60,8 @@ class Http {
}
public get(url: string, query = {}, config: ConfigOption = {}) {
return this.request(url, 'GET', { query }, config)
url += '?' + qs.stringify(query)
return this.request(url, 'GET', {}, config)
}
public post(url: string, body = {}, config: ConfigOption = {}) {
@ -78,8 +80,8 @@ class Http {
*
* @param url
* @param method
* @param showMessageConfig
* @returns
* @param param
* @param config
*/
private request(url: string, method: string, param: AnyObject = {}, config: ConfigOption = {}) {
return new Promise((resolve, reject) => {
@ -88,7 +90,6 @@ class Http {
const runtimeConfig = useRuntimeConfig()
!this.options.baseURL && (this.options.baseURL = runtimeConfig.public.VITE_APP_BASE_URL || `${location.origin}/api/`)
this.options.baseURL.substr(-1) != '/' && (this.options.baseURL += '/')
// 处理数组格式
for (const key in param.query) {
if (param.query[key] instanceof Array) {
@ -98,7 +99,6 @@ class Http {
delete param.query[key]
}
}
useFetch(url, { ...this.options, method, ...config, ...param }).then((response) => {
const { data: { value }, error } = response
if (value) {

View File

@ -222,8 +222,7 @@ const test = {
},
/**
*
* @param {Object}
* @return {Boolean}
* @param o
*/
regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]'