update web

This commit is contained in:
全栈小学生 2023-09-06 18:40:52 +08:00
parent a7f7a31bb1
commit 94c19e2ee3
58 changed files with 282 additions and 452 deletions

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

@ -13,7 +13,6 @@ import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import en from 'element-plus/dist/locale/en.mjs'
import Language from '~~/utils/language'
import useSystemStore from '@/stores/system'
import useAppStore from '@/stores/app'
import useMemberStore from '@/stores/member'
//
import '@/assets/styles/index.scss'
@ -32,22 +31,21 @@ const locale = computed(() => (systemStore.lang === 'zh-cn' ? zhCn : en))
const configStore = useConfigStore()
configStore.getLoginConfig()
//
systemStore.getSitenfo()
//
getToken() && useMemberStore().setToken(getToken())
const route = useRoute()
watch(route, (nval, oval) => {
useAppStore().$patch(state => {
state.route = route.path
})
//
const fileinfo = nval.matched[0].components.default.__file.split('/pages/')
const file = fileinfo[1].replace('.vue', '')
const app = fileinfo[0].substring(fileinfo[0].lastIndexOf('/') + 1)
const language = new Language(useNuxtApp().$getI18n())
language.loadLocaleMessages(app, file, useSystemStore().lang)
// title
let path = route.path == '/' ? '/index' : route.path
// url /
if (path.slice(-1) == '/') path = path.slice(0, -1)
path = !path.lastIndexOf('/') ? `${path}/index` : path
let key = path.replace('/', '').replaceAll('/', '.')
@ -58,10 +56,6 @@ watch(route, (nval, oval) => {
}, !oval ? 500 : 0)
}, { immediate: true })
//
const language = new Language(useNuxtApp().$getI18n())
language.loadLocaleMessages(route.path, useSystemStore().lang)
// title
useHead({
titleTemplate: (title) => {

View File

@ -66,11 +66,4 @@ export function fetchBase64Image(data: AnyObject) {
*/
export function getCopyRight(data: AnyObject) {
return request.get('copyright', data)
}
/**
*
*/
export function getSiteInfo() {
return request.get('site')
}

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,2 @@
{
}

View File

@ -0,0 +1,33 @@
{
"login": "登录",
"register": "注册",
"getSmsCode": "获取短信验证码",
"smsCodeChangeText": "秒后重新获取",
"captchaTitle": "请先完成安全验证",
"confirm": "确认",
"cancel": "取消",
"captchaPlaceholder": "请输入验证码",
"mobilePlaceholder": "请输入手机号码",
"mobileError": "请输入正确的手机号",
"codePlaceholder": "请输入手机验证码",
"userAgreement": "用户协议",
"privacyAgreement": "隐私协议",
"protocolNotConfigured": "未配置协议",
"request": {
"unknownError": "未知错误",
"400": "错误的请求",
"401": "请重新登录",
"403": "拒绝访问",
"404": "请求错误",
"405": "请求方法未允许",
"408": "请求超时",
"409": "请求跨域",
"500": "服务器端出错,错误原因:",
"501": "网络未实现",
"502": "网络错误",
"503": "服务不可用",
"504": "网络超时",
"505": "http版本不支持该请求",
"timeout": "网络请求超时!"
}
}

View File

@ -5,20 +5,12 @@
},
"auth": {
"login": "登录",
"register": "登录",
"register": "注册",
"bind": "手机号绑定"
},
"member": {
"index": "欢迎页",
"center": "个人中心"
},
"article": {
"list": "文章",
"detail": "文章"
},
"site": {
"close": "站点已关闭",
"nosite": "站点不存在"
}
}
}

View File

@ -11,7 +11,7 @@
</template>
<script lang="ts" setup>
import { getAgreementInfo } from '@/api/system'
import { getAgreementInfo } from '@/app/api/system'
const agreement = ref<any | null>(null)
const route = useRoute()
@ -26,4 +26,4 @@ getAgreementInfo(route.query.key).then(({ data }) => {
})
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>

View File

@ -1,10 +1,10 @@
<template>
<div class="w-full h-full bg-page flex items-center justify-center">
<div class="flex bg-white">
<div class="bg-white w-[380px] p-[30px]">
<div class="flex items-end mb-[30px] mt-[15px]">
<div class="mr-[20px] text-base cursor-pointer leading-none font-bold">{{t('mobileBind')}}</div>
<div class="mr-[20px] text-base cursor-pointer leading-none font-bold">{{ t('mobileBind') }}</div>
</div>
<el-form :model="formData" ref="formRef" :rules="formRules" :validate-on-rule-change="false">
<div>
@ -15,14 +15,16 @@
<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>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key"
@click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</el-form-item>
</div>
<el-form-item>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleRegister" :loading="loading">{{ loading ? t('binding') : t('bind') }}</el-button>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleRegister"
:loading="loading">{{ loading ? t('binding') : t('bind') }}</el-button>
</el-form-item>
</el-form>
</div>
@ -32,8 +34,8 @@
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { bind } from '@/api/auth'
import { bindMobile } from '@/api/member'
import { bind } from '@/app/api/auth'
import { bindMobile } from '@/app/api/member'
import useMemberStore from '@/stores/member'
import { FormInstance } from 'element-plus'
definePageMeta({
@ -61,7 +63,7 @@ const formRules = computed(() => {
},
{
validator(rule: any, value: string, callback: any) {
return test.mobile(value)
return test.mobile(value)
},
message: t('mobileError'),
trigger: ['change', 'blur'],
@ -135,4 +137,4 @@ const sendSmsCode = async () => {
:deep(.el-form-item__error) {
padding-top: 5px;
}
</style>
</style>~/app/api/auth~/app/api/member

View File

@ -7,7 +7,8 @@
<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">
<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>
@ -17,16 +18,19 @@
<div class="bg-white w-[380px] p-[30px]">
<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 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>
<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">
</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">
</el-input>
</el-form-item>
</div>
@ -38,7 +42,8 @@
<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>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key"
@click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</el-form-item>
@ -54,7 +59,8 @@
</div>
<el-form-item>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleLogin" :loading="loading">{{ loading ? t('logining') : t('login') }}</el-button>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleLogin"
:loading="loading">{{ loading ? t('logining') : t('login') }}</el-button>
</el-form-item>
<div class="text-xs py-[50rpx] flex justify-center w-full" v-if="configStore.login.agreement_show">
@ -77,15 +83,11 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { FormInstance } from 'element-plus'
import { usernameLogin, mobileLogin, scanlogin, checkscan } from '@/api/auth'
import { usernameLogin, mobileLogin, scanlogin, checkscan } from '@/app/api/auth'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import QRCode from "qrcode";
definePageMeta({
layout: "container"
});
//
const checkScanFn = (key) => {
let parameter = { key };
@ -119,7 +121,7 @@ const checkScanFn = (key) => {
})
}
// ,
// ,
const weixinCode = ref({
url: '',
key: '',
@ -226,7 +228,7 @@ const sendSmsCode = async () => {
})
}
</script>
<style lang="scss" scoped>
:deep(.el-form-item) {
.el-input__wrapper {
@ -254,4 +256,4 @@ const sendSmsCode = async () => {
.text-color {
color: var(--el-color-primary);
}
</style>
</style>~/app/api/auth

View File

@ -5,26 +5,32 @@
<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>
<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="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 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">
</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">
</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">
</el-input>
</el-form-item>
</div>
@ -36,7 +42,8 @@
<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>
<sms-code :mobile="formData.mobile" type="login" v-model="formData.mobile_key"
@click="sendSmsCode" ref="smsCodeRef"></sms-code>
</template>
</el-input>
</el-form-item>
@ -46,7 +53,8 @@
<el-input v-model="formData.captcha_code" :placeholder="t('captchaPlaceholder')">
<template #suffix>
<div class="py-0 leading-none">
<el-image :src="captcha.image.value" class="h-[30px] cursor-pointer" @click="captcha.refresh()"></el-image>
<el-image :src="captcha.image.value" class="h-[30px] cursor-pointer"
@click="captcha.refresh()"></el-image>
</div>
</template>
</el-input>
@ -60,7 +68,8 @@
</div>
<el-form-item>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleRegister" :loading="loading">{{ loading ? t('registering') : t('register') }}</el-button>
<el-button type="primary" class="mt-[20px] w-full" size="large" @click="handleRegister"
:loading="loading">{{ loading ? t('registering') : t('register') }}</el-button>
</el-form-item>
<div class="text-xs py-[50rpx] flex justify-center w-full" v-if="configStore.login.agreement_show">
@ -80,7 +89,7 @@
</template>
<script lang="ts" setup>
import { usernameRegister, mobileRegister } from '@/api/auth'
import { usernameRegister, mobileRegister } from '@/app/api/auth'
import useMemberStore from '@/stores/member'
import useConfigStore from '@/stores/config'
import { FormInstance } from 'element-plus'
@ -234,5 +243,4 @@ const sendSmsCode = async () => {
:deep(.el-form-item__error) {
padding-top: 5px;
}
</style>
}</style>~/app/api/auth

View File

@ -13,7 +13,7 @@
<div class="flex justify-between mt-[100px]">
<div class="w-[280px]">
<div class="flex items-center">
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/course.jpg" /></div>
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/app/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]">
@ -27,7 +27,7 @@
</div>
<div class="w-[280px]">
<div class="flex items-center">
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/api.jpg" /></div>
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/app/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]">
@ -41,7 +41,7 @@
</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>
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/app/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]">
@ -55,7 +55,7 @@
</div>
<div class="w-[280px]">
<div class="flex items-center">
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/assets/images/word/wx.jpg"></div>
<div class="w-[30px] h-[30px] mr-[10px]"><img src="@/app/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]">
@ -79,8 +79,8 @@
}
.word {
background-image: url(@/assets/images/word/word-back.jpg);
background-image: url(@/app/assets/images/word/word-back.jpg);
background-size: 100%;
background-repeat: no-repeat;
}
</style>
</style>

View File

@ -5,7 +5,7 @@
<el-card class="box-card flex-1 ml-4" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('myBalance')}}</span>
<span>{{ t('myBalance') }}</span>
</div>
</template>
<div class="px-6">
@ -17,7 +17,10 @@
<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" />
<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>
</div>
</el-card>
@ -28,7 +31,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { UploadProps } from 'element-plus'
import { getBalanceList } from '@/api/member'
import { getBalanceList } from '@/app/api/member'
//
const balanceTableData = reactive({
page: 1,
@ -57,10 +60,11 @@ loadBalanceList()
</script>
<style lang="scss" scoped>
.box-card{
.box-card {
border: none !important;
}
.text-color{
.text-color {
color: var(--jjext-color-brand);
}
</style>
</style>~/app/api/member

View File

@ -12,7 +12,8 @@
<el-form :model="info" class="form-wrap" label-width="120px">
<el-form-item :label="t('memberHeadimg')">
<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-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>
@ -22,7 +23,8 @@
<el-form-item :label="t('nickname')">
<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 class="cursor-pointer text-color" @click="updateNickname.modal = true">{{
t('edit') }}</span>
</div>
</el-form-item>
</el-form>
@ -50,7 +52,7 @@
import { reactive, ref, computed } from 'vue'
import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
import { modifyMember } from '@/api/member'
import { modifyMember } from '@/app/api/member'
import { ElMessage, UploadFile, UploadFiles } from 'element-plus'
import request from '@/utils/request'
import storage from '@/utils/storage'
@ -77,7 +79,6 @@ definePageMeta({ middleware: 'auth' })
const upload = computed(() => {
const headers: Record<string, any> = {}
headers.token = getToken()
headers['site-id'] = storage.get('siteId') || 1
return {
action: `${request.options.baseURL}/file/image`,
limit: 1,
@ -120,4 +121,4 @@ const updateNicknameConfirm = () => {
::v-deep .form-wrap .el-form-item {
align-items: center;
}
</style>
</style>~/app/api/member

View File

@ -5,19 +5,21 @@
<el-card class="box-card flex-1 ml-4" v-loading="loading" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('welcomePage')}}</span>
<span>{{ t('welcomePage') }}++++++</span>
</div>
</template>
<div class="px-10 py-5" v-if="info">
<div class="flex items-center border-gray-300 border-b-1 pb-5 px-5">
<img v-if="!info.headimg" class="w-[65px] h-[65px] rounded-full" src="@/assets/images/default_headimg.png" alt="">
<img v-if="!info.headimg" class="w-[65px] h-[65px] rounded-full"
src="@/assets/images/default_headimg.png" alt="">
<img v-else :src="img(info.headimg)" class="w-[65px] h-[65px] rounded-full" alt="">
<div class="ml-4">
<div>
<span class="text-base font-bold">{{info.nickname}}</span>
<span class="text-xs">{{t('mobile')}}{{info.mobile ? info.mobile : t('notBound') }}</span>
<span class="text-base font-bold">{{ info.nickname }}</span>
<span class="text-xs">{{ t('mobile') }}{{ info.mobile ? info.mobile : t('notBound')
}}</span>
</div>
<p class="text-xs text-gray-400 mt-1">{{t('registrationTime')}}{{info.create_time}}</p>
<p class="text-xs text-gray-400 mt-1">{{ t('registrationTime') }}{{ info.create_time }}</p>
</div>
</div>
<div class="flex justify-between mt-8 statistic-wrap px-8">
@ -41,24 +43,30 @@ import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
import type { UploadProps } from 'element-plus'
definePageMeta({ middleware: 'auth' })
const memberStore = useMemberStore()
const loading = ref(true)
const info = computed(() =>{
if(memberStore.info) loading.value = false;
const info = computed(() => {
if (memberStore.info) loading.value = false;
return memberStore.info;
})
const appStore = useAppStore()
</script>
<style lang="scss" scoped>
::v-deep .box-card{
::v-deep .box-card {
border: none !important;
.el-card__header{
.el-card__header {
border-color: #F1F1F1;
}
}
::v-deep .statistic-wrap{
.el-statistic__head, .el-statistic__content{
::v-deep .statistic-wrap {
.el-statistic__head,
.el-statistic__content {
text-align: center;
}
}

View File

@ -5,7 +5,7 @@
<el-card class="box-card flex-1 ml-4" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('myPoint')}}</span>
<span>{{ t('myPoint') }}</span>
</div>
</template>
<div class="px-6">
@ -17,7 +17,9 @@
<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" />
<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>
</div>
</el-card>
@ -28,7 +30,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import type { UploadProps } from 'element-plus'
import { getPointList } from '@/api/member'
import { getPointList } from '@/app/api/member'
//
const pointTableData = reactive({
page: 1,
@ -57,10 +59,10 @@ loadPointList()
</script>
<style lang="scss" scoped>
.box-card{
.box-card {
border: none !important;
}
.text-color{
.text-color {
color: var(--jjext-color-brand);
}
</style>
}</style>~/app/api/member

59
web/app/pages/routes.ts Normal file
View File

@ -0,0 +1,59 @@
export default [
{
path: "/",
component: () => import('~/app/pages/index.vue')
},
{
path: "/auth/login",
component: () => import('~/app/pages/auth/login.vue'),
meta: {
layout: "container"
}
},
{
path: "/auth/register",
component: () => import('~/app/pages/auth/register.vue'),
meta: {
layout: "container"
}
},
{
path: "/auth/bind",
component: () => import('~/app/pages/auth/bind.vue'),
meta: {
layout: "container"
}
},
{
path: "/auth/agreement",
component: () => import('~/app/pages/auth/agreement.vue')
},
{
path: "/member",
component: () => import('~/app/pages/member/index.vue'),
meta: {
middleware: ["auth"]
}
},
{
path: "/member/center",
component: () => import('~/app/pages/member/center.vue'),
meta: {
middleware: ["auth"]
}
},
{
path: "/member/balance",
component: () => import('~/app/pages/member/balance.vue'),
meta: {
middleware: ["auth"]
}
},
{
path: "/member/point",
component: () => import('~/app/pages/member/point.vue'),
meta: {
middleware: ["auth"]
}
}
]

16
web/app/router.options.ts Normal file
View File

@ -0,0 +1,16 @@
import type { RouterConfig } from '@nuxt/schema'
const routeFiles = import.meta.glob('../**/routes.ts')
const routes = []
for (const key of Object.keys(routeFiles)) {
await routeFiles[key]().then(res => {
routes.push(...res.default)
})
}
// https://router.vuejs.org/api/interfaces/routeroptions.html
export default <RouterConfig>{
routes: (_routes) => routes,
strict: false
}

View File

@ -1,4 +1,4 @@
import { getCaptcha } from '@/api/system'
import { getCaptcha } from '~/app/api/system'
interface formData {
captcha_code: string,

View File

@ -2,10 +2,7 @@ import useAppStore from '~/stores/app'
export function t(message: string) {
const i18n = useNuxtApp().$getI18n()
let path = useAppStore().route
// 处理部署后不知道为什么url会自动拼接上 / 的问题
if (path != '/' && path.slice(-1) == '/') path = path.slice(0, -1)
const file = path == '/' ? 'index' : path.replace('/', '').replaceAll('/', '.')
const key = `${file}.${message}`
const langKey = useAppStore().langKey
const key = `${langKey}.${message}`
return i18n.global.t(key) != key ? i18n.global.t(key) : i18n.global.t(message)
}

View File

@ -1,4 +1,4 @@
import { sendSms } from '@/api/system'
import { sendSms } from '~/app/api/system'
export function useSendSms() {
const canGetCode = ref(true),

6
web/env/.env.dev vendored
View File

@ -4,17 +4,11 @@ VITE_APP_BASE_URL=''
# 图片服务器地址
VITE_IMG_DOMAIN=''
# 本地开发时站点id
VITE_SITE_ID = 1
# 本地存储时token的参数名
VITE_REQUEST_STORAGE_TOKEN_KEY='webToken'
# 请求时header中token的参数名
VITE_REQUEST_HEADER_TOKEN_KEY='token'
# 请求时header中站点的参数名
VITE_REQUEST_HEADER_SITEID_KEY='site-id'
# 请求时header中来源场景的参数名
VITE_REQUEST_HEADER_CHANNEL_KEY='channel'

View File

@ -4,17 +4,11 @@ VITE_APP_BASE_URL=''
# 图片服务器地址
VITE_IMG_DOMAIN=''
# 本地开发时站点id
VITE_SITE_ID = 1
# 本地存储时token的参数名
VITE_REQUEST_STORAGE_TOKEN_KEY='webToken'
# 请求时header中token的参数名
VITE_REQUEST_HEADER_TOKEN_KEY='token'
# 请求时header中站点的参数名
VITE_REQUEST_HEADER_SITEID_KEY='site-id'
# 请求时header中来源场景的参数名
VITE_REQUEST_HEADER_CHANNEL_KEY='channel'

View File

@ -1,3 +0,0 @@
{
"title": "文章"
}

View File

@ -1,3 +0,0 @@
{
"title": "文章"
}

View File

@ -13,8 +13,6 @@
"userAgreement": "用户协议",
"privacyAgreement": "隐私协议",
"protocolNotConfigured": "未配置协议",
"siteClose": "站点已关闭",
"noSite": "站点不存在",
"request": {
"unknownError": "未知错误",
"400": "错误的请求",

View File

@ -1,53 +1,51 @@
<template>
<div class="flex h-[220px] min-w-[1200px] bg-[#3F4045]">
<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]">
<span>友情链接</span>
<NuxtLink to="https://www.bt.cn">
<span class="mr-[10px]">宝塔</span>|
<span class="mr-[10px]">宝塔</span>|
</NuxtLink>
<NuxtLink to="https://www.oschina.net">
<span class="mr-[10px]">开源中国</span>|
<span class="mr-[10px]">开源中国</span>|
</NuxtLink>
<NuxtLink to="https://www.aliyun.com">
<span class="mr-[10px]">阿里云</span>|
<span class="mr-[10px]">阿里云</span>|
</NuxtLink>
<NuxtLink to="https://gitee.com/">
<span class="mr-[10px]">码云Gitee</span>|
<span class="mr-[10px]">码云Gitee</span>|
</NuxtLink>
<NuxtLink to="https://cloud.tencent.com/">
<span class="mr-[10px]">腾讯云</span>|
<span class="mr-[10px]">腾讯云</span>|
</NuxtLink>
<NuxtLink to="https://mp.weixin.qq.com">
<span class="mr-[10px]">微信公众平台</span>|
<span class="mr-[10px]">微信公众平台</span>|
</NuxtLink>
<NuxtLink to="http://www.thinkphp.cn">
<span class="mr-[10px]">Thinkphp</span>
<span class="mr-[10px]">Thinkphp</span>
</NuxtLink>
</p>
<p class="text-center mt-[20px] text-[#999]">
<p class="text-center mt-[20px] text-[#999]">
<NuxtLink to="https://beian.miit.gov.cn/">
<span>备案号:{{ copy }}</span>
</NuxtLink>
</p>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { getCopyRight } from '@/api/system';
import { getCopyRight } from '@/app/api/system';
import { reactive, ref } from 'vue'
const copy = ref();
const getCopy = () => {
getCopyRight({
}).then(res => {
getCopyRight({
}).then(res => {
copy.value = res.data.icp
})
})
}
getCopy()
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>

View File

@ -9,12 +9,12 @@
</div>
<div class="mx-auto flex-shrink">
<el-menu :default-active="appStore.route" class="h-full" mode="horizontal" :ellipsis="false" :router="true">
<el-menu :default-active="route.path" class="h-full" mode="horizontal" :ellipsis="false" :router="true">
<el-menu-item index="/" route="/">
<span class="text-base mx-4">首页</span>
<span></span>
</el-menu-item>
<el-menu-item index="/article/list" route="/article/list">
<el-menu-item index="/cms/article/list" route="/cms/article/list">
<span class="text-base mx-4">文章</span>
<span></span>
</el-menu-item>
@ -42,7 +42,6 @@
<script lang="ts" setup>
import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
const memberStore = useMemberStore()
const info = computed(() => memberStore.info)
@ -52,7 +51,7 @@ const logoutFn = () => {
navigateTo(`/auth/login`)
}
const appStore = useAppStore()
const route = useRoute()
</script>
<style lang="scss" scoped>

View File

@ -1,7 +0,0 @@
import useSystemStore from '~/stores/system'
import Language from '~~/utils/language'
export default defineNuxtRouteMiddleware((to, from) => {
const language = new Language(useNuxtApp().$getI18n())
language.loadLocaleMessages(to.path, useSystemStore().lang)
})

View File

@ -1,5 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
import { loadEnv } from 'vite'
import topLevelAwait from 'vite-plugin-top-level-await'
const envScript = (process.env as any).npm_lifecycle_script.split(' ')
const envName = envScript[envScript.length - 1]
@ -18,6 +19,14 @@ export default defineNuxtConfig({
},
vite: {
envDir: '~/env',
plugins: [
topLevelAwait({
// The export name of top-level await promise for each chunk module
promiseExportName: '__tla',
// The function to generate import names of top-level await promise in each chunk module
promiseImportName: i => `__tla_${i}`
})
]
},
ssr: false
})

View File

@ -15,6 +15,7 @@
"nuxt": "^3.4.1",
"nuxt-windicss": "^2.6.0",
"sass": "^1.60.0",
"vite-plugin-top-level-await": "^1.3.1",
"vue-i18n": "^9.2.2"
},
"dependencies": {
@ -24,4 +25,4 @@
"qrcode": "^1.5.1",
"sass": "^1.60.0"
}
}
}

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 '@/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 '@/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

@ -2,15 +2,13 @@ import { createI18n } from 'vue-i18n'
import zhCn from "~/lang/zh-cn/common.json";
import en from "~/lang/en/common.json"
import zhCnPages from "~/lang/zh-cn/pages.json";
import enPages from "~/lang/en/pages.json"
export default defineNuxtPlugin((NuxtApp) => {
const i18n = createI18n({
globalInjection: true, //是否全局注入
messages: {
"zh-cn": Object.assign(zhCn, zhCnPages),
"en": Object.assign(en, enPages)
"zh-cn": zhCn,
"en": en
},
silentFallbackWarn: true,
silentTranslationWarn: true

View File

@ -1,13 +1,13 @@
import { defineStore } from 'pinia'
interface App {
route: string
langKey: string
}
const useAppStore = defineStore('app', {
state: (): App => {
return {
route: ''
langKey: ''
}
},
actions: {

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { getConfig } from '@/api/auth'
import { getConfig } from '~/app/api/auth'
interface loginConfig {
is_username: number | boolean,

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { getMemberInfo } from '@/api/member'
import { logout } from '@/api/auth'
import { getMemberInfo } from '~/app/api/member'
import { logout } from '~/app/api/auth'
interface Member {
token: string | null
@ -21,6 +21,7 @@ const useMemberStore = defineStore('member', {
await this.getMemberInfo()
},
async getMemberInfo() {
if (!this.token) return
await getMemberInfo()
.then((res: any) => {
this.info = res.data
@ -30,6 +31,7 @@ const useMemberStore = defineStore('member', {
})
},
logout() {
if (!this.token) return
logout().then(() => {
this.$reset()
useCookie('token').value = null

View File

@ -1,6 +1,5 @@
import { defineStore } from 'pinia'
import storage from '@/utils/storage'
import { getSiteInfo } from '@/api/system'
interface System {
lang: string,
@ -18,16 +17,7 @@ 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 })
})
}
}
})

View File

@ -1,4 +1,5 @@
import { nextTick } from 'vue'
import useAppStore from '~/stores/app'
class Language {
private i18n: any;
@ -28,27 +29,41 @@ class Language {
* @param locale
* @returns
*/
public async loadLocaleMessages(path: string, locale: string) {
public async loadLocaleMessages(app: string, path: string, locale: string) {
try {
const file = path == '/' ? 'index' : path.replace('/', '').replaceAll('/', '.')
const file = path.replaceAll('/', '.')
if (this.loadLocale.includes(`${locale}/${file}`)) {
if (this.loadLocale.includes(`${app}/${locale}/${file}`)) {
return nextTick()
}
this.loadLocale.push(`${locale}/${file}`)
// 加载pages语言包
if (!this.loadLocale.includes(`${app}/${locale}/pages`)) {
// 引入语言包文件
const pagesMessages = await import(`@/${app}/lang/${locale}/pages.json`)
this.i18n.global.mergeLocaleMessage(locale, pagesMessages.default)
this.loadLocale.push(`${app}/${locale}/pages`)
}
this.loadLocale.push(`${app}/${locale}/${file}`)
// 引入语言包文件
const messages = await import(`~/lang/${locale}/${file}.json`)
const messages = await import(`@/${app}/lang/${locale}/${file}.json`)
let data: Record<string, string> = {}
Object.keys(messages.default).forEach(key => {
data[`${file}.${key}`] = messages.default[key]
data[`${app}.${file}.${key}`] = messages.default[key]
})
useAppStore().$patch(state => {
state.langKey = `${app}.${file}`
})
this.i18n.global.mergeLocaleMessage(locale, data)
this.setI18nLanguage(locale)
return nextTick()
} catch {
} catch (e) {
console.log(e)
this.setI18nLanguage(locale)
return nextTick()
}

View File

@ -34,7 +34,6 @@ class Http {
const runtimeConfig = useRuntimeConfig()
this.options.baseURL = runtimeConfig.public.VITE_APP_BASE_URL || `${location.origin}/api/`
this.options.headers[runtimeConfig.public.VITE_REQUEST_HEADER_SITEID_KEY] = useCookie('siteId').value || runtimeConfig.public.VITE_SITE_ID
this.options.headers[runtimeConfig.public.VITE_REQUEST_HEADER_CHANNEL_KEY] = 'pc'
if (getToken()) this.options.headers[runtimeConfig.public.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
}