更新web端

This commit is contained in:
全栈小学生 2023-05-20 18:05:21 +08:00
parent fa37c55a5d
commit e2537e99b0
19 changed files with 8909 additions and 34918 deletions

1
web/.gitignore vendored
View File

@ -1,5 +1,4 @@
node_modules
*.log*
.nuxt
.nitro
.cache

View File

@ -5,27 +5,27 @@ export function getMemberInfo() {
/**
*
*/
export function updateMember(data : AnyObject) {
export function modifyMember(data: AnyObject) {
return request.put(`member/modify/${data.field}`, data, { showErrorMessage: true })
}
/**
*
*/
export function getPointList(data : AnyObject) {
export function getPointList(data: AnyObject) {
return request.get('member/account/point', data)
}
/**
*
*/
export function getBalanceList(data : AnyObject) {
export function getBalanceList(data: AnyObject) {
return request.get('member/account/balance', data)
}
/**
*
*/
export function bindMobile(data : AnyObject) {
export function bindMobile(data: AnyObject) {
return request.put('member/mobile', data, { showErrorMessage: true })
}

View File

@ -47,13 +47,6 @@ export function getWechatSkdConfig(data: AnyObject) {
return request.get('wechat/jssdkconfig', data)
}
/**
*
*/
export function uploadImage(data: AnyObject) {
return request.upload('file/image', data, { showErrorMessage: true })
}
/**
*
*/

View File

@ -18,6 +18,12 @@ import useMemberStore from '@/stores/member'
//
import '@/assets/styles/index.scss'
if (process.client) {
const match = location.href.match(/\/s(\d*)\//)
const cookie = useCookie('siteId')
match ? cookie.value = match[1] : cookie.value = null
}
//
const systemStore = useSystemStore()
const locale = computed(() => (systemStore.lang === 'zh-cn' ? zhCn : en))
@ -37,8 +43,10 @@ watch(route, (nval, oval) => {
// title
let path = route.path == '/' ? '/index' : route.path
// url /
if (path.slice(-1) == '/') path = path.slice(0, -1)
path = !path.lastIndexOf('/') ? `${path}/index` : path
const key = path.replace('/', '').replaceAll('/', '.')
let key = path.replace('/', '').replaceAll('/', '.')
setTimeout(() => {
useHead({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 268 KiB

View File

@ -73,6 +73,7 @@ captcha.refresh()
const sendSms = useSendSms()
const send = () => {
formData.mobile = prop.mobile
if (sendSms.canGetCode.value) {
captchaDialog.value = true
}
@ -87,8 +88,10 @@ const confirm = async () => {
if (sendRes) {
value.value = sendRes
captchaDialog.value = false
loading.value = false
} else if (sendRes === false) {
captcha.refresh()
loading.value = false
}
}
})

View File

@ -2,7 +2,9 @@ import useAppStore from '~/stores/app'
export function t(message: string) {
const i18n = useNuxtApp().$getI18n()
const path = useAppStore().route
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}`
return i18n.global.t(key) != key ? i18n.global.t(key) : i18n.global.t(message)

View File

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

View File

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

View File

@ -0,0 +1,13 @@
{
"logining": "登录中",
"usernamePlaceholder": "请输入账号",
"passwordPlaceholder": "请输入密码",
"resetpwd": "忘记密码",
"noAccount": "还没有账号",
"toRegister": "去注册",
"and": "和",
"agreeTips": "登录代表您同意",
"usernameLogin": "密码登录",
"mobileLogin": "验证码登录",
"mobilePlaceholder": "请输入手机号"
}

View File

@ -11,6 +11,10 @@
"member": {
"index": "欢迎页",
"center": "个人中心"
}
},
"article": {
"list": "文章",
"detail": "文章"
}
}
}

View File

@ -1,13 +1,13 @@
<template>
<!-- main-container -->
<!-- main-container -->
<div class="flex h-full min-w-[1200px]">
<div class="flex items-center ml-[20px]">
<NuxtLink to="/">
<div class="w-[132px] mr-[10px]"><img src="@/assets/images/index/logo.jpg" /></div>
</NuxtLink>
<div class="hidden text-[14px] text-[#A6B0C8] xl:block">|<span class="ml-[10px]">一款快速开发SAAS通用管理系统后台框架</span>
</div>
</div>
<NuxtLink to="/">
<div class="w-[132px] mr-[10px]"><img src="@/assets/images/index/logo.jpg" /></div>
</NuxtLink>
<div class="hidden text-[14px] text-[#A6B0C8] xl:block">|<span class="ml-[10px]">一款快速开发SAAS通用管理系统后台框架</span>
</div>
</div>
<div class="mx-auto flex-shrink">
<el-menu :default-active="appStore.route" class="h-full" mode="horizontal" :ellipsis="false" :router="true">
@ -15,11 +15,11 @@
<span class="text-base mx-4">首页</span>
<span></span>
</el-menu-item>
<el-menu-item index="/community/answer" route="/">
<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="/">
<el-menu-item route="/">
<span class="text-base mx-4">社区</span>
<span></span>
</el-menu-item>
@ -27,15 +27,13 @@
</div>
<div class="flex items-center justify-end mr-[20px] ml-auto whitespace-pre">
<el-dropdown v-if="info">
<div>
<NuxtLink to="/member/center">
<span class="cursor-pointer">{{info.nickname}}</span>
</NuxtLink>
<span class="mx-2">|</span>
<span class="cursor-pointer" @click="logoutFn">退出</span>
</div>
</el-dropdown>
<div v-if="info">
<NuxtLink to="/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>
@ -50,7 +48,7 @@ import useAppStore from '@/stores/app'
const memberStore = useMemberStore()
const info = computed(() => memberStore.info)
const logoutFn = ()=>{
const logoutFn = () => {
memberStore.logout()
navigateTo(`/auth/login`)
}
@ -62,23 +60,27 @@ const appStore = useAppStore()
:deep(.el-menu--horizontal) {
border-bottom: none;
}
.el-menu-item{
.el-menu-item {
padding-left: 0;
border: none !important;
color: #000 !important;
&.is-active{
&.is-active {
border: none !important;
color: #000 !important;
span{
&:first-of-type{
span {
&:first-of-type {
position: relative;
z-index: 1;
}
&:last-of-type{
&:last-of-type {
position: absolute;
width: 16px;
height: 16px;
background-image: linear-gradient(to bottom right,#FFFFFF,var(--el-color-primary));
background-image: linear-gradient(to bottom right, #FFFFFF, var(--el-color-primary));
border-radius: 100px;
bottom: 15px;
right: 27px;
@ -86,11 +88,13 @@ const appStore = useAppStore()
}
}
}
&:hover{
&:hover {
background-color: transparent !important;
color: var(--el-menu-hover-text-color) !important;
}
&:focus{
&:focus {
background-color: transparent !important;
}
}

43336
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,27 @@
{
"name": "web",
"private": true,
"version": "1.0.0",
"scripts": {
"build": "nuxt build --mode product",
"dev": "nuxt dev --mode dev",
"generate": "nuxt generate --mode product",
"preview": "nuxt preview --mode product",
"postinstall": "nuxt prepare --mode product"
},
"devDependencies": {
"@element-plus/nuxt": "^1.0.4",
"nuxt": "^3.3.2",
"nuxt-windicss": "^2.6.0",
"sass": "^1.60.0",
"vue-i18n": "^9.2.2",
"@types/qrcode": "^1.5.0"
},
"dependencies": {
"@vueuse/core": "^9.13.0",
"element-plus": "^2.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1"
}
}
"name": "web",
"private": true,
"version": "1.0.0",
"scripts": {
"build": "nuxt build --mode product",
"dev": "nuxt dev --mode dev",
"generate": "nuxt generate --mode product",
"preview": "nuxt preview --mode product",
"postinstall": "nuxt prepare --mode product"
},
"devDependencies": {
"@element-plus/nuxt": "^1.0.4",
"@types/qrcode": "^1.5.0",
"nuxt": "^3.4.1",
"nuxt-windicss": "^2.6.0",
"sass": "^1.60.0",
"vue-i18n": "^9.2.2"
},
"dependencies": {
"@vueuse/core": "^9.13.0",
"element-plus": "^2.3.2",
"pinia": "^2.0.33",
"qrcode": "^1.5.1",
"sass": "^1.60.0"
}
}

View File

@ -0,0 +1,80 @@
<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(() => {
obtainArricleInfo(Route.query.id)
});
const obtainArricleInfo = (id) => {
getArticleDetail(id).then(res => {
console.log(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);
}
// .demo-tabs .el-ta
.custom-tabs-label span{
font-size: 20px;
padding: 0px 10px;
}
</style>

172
web/pages/article/list.vue Normal file
View File

@ -0,0 +1,172 @@
<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 activeCategotyLsit" :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, computed } from 'vue'
import { bind } from '@/api/auth'
import { bindMobile } from '@/api/member'
import useMemberStore from '@/stores/member'
import { getArticleCategory,getArticleList } from '@/api/article'
import { FormInstance } from 'element-plus'
import { ArrowRight } from '@element-plus/icons-vue'
import type { TabsPaneContext } from 'element-plus'
import { useRouter } from 'vue-router';
const router = useRouter();
const activeCategotyLsit = ref([])
const selectedCategoryName = ref()
const articleTableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: true,
data: [],
searchParam: {
title: '',
category_id:''
}
})
const handleClick = (tab: TabsPaneContext, event: Event) => {
console.log(tab, event)
}
/**
* 获取文章列表
*/
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 => {
activeCategotyLsit.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);
}
// .demo-tabs .el-ta
.custom-tabs-label span{
font-size: 20px;
padding: 0px 10px;
}
.activeBo {
bottom : 20px;
right : 0px
}
</style>

View File

@ -194,9 +194,6 @@ const formRules = computed(() => {
}
}
})
console.log(
"memberStore.setToken", memberStore
)
const handleLogin = async () => {
await formRef.value?.validate(async (valid, fields) => {

View File

@ -5,28 +5,26 @@
<el-card class="box-card flex-1 ml-4" v-loading="loading" shadow="never">
<template #header>
<div class="card-header">
<span>{{t('personageInfo')}}</span>
<span>{{ t('personageInfo') }}</span>
</div>
</template>
<div class="pr-15" v-if="info">
<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="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>
<el-upload class="avatar-uploader" :show-file-list="false" v-bind="upload">
<span class="cursor-pointer text-color">{{ t('edit') }}</span>
</el-upload>
</div>
</el-form-item>
<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>{{ updateNickname.value }}</span>
<span class="cursor-pointer text-color" @click="updateNickname.modal = true">{{ t('edit')
}}</span>
</div>
</el-form-item>
</el-form>
@ -41,8 +39,8 @@
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="updateNickname.modal = false">{{t('cancel')}}</el-button>
<el-button type="primary" @click="updateNicknameConfirm">{{t('confirm')}}</el-button>
<el-button @click="updateNickname.modal = false">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="updateNicknameConfirm">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
@ -54,9 +52,7 @@
import { reactive, ref, computed } from 'vue'
import useMemberStore from '@/stores/member'
import useAppStore from '@/stores/app'
import { updateMember } from '@/api/member'
import { uploadImage } from '@/api/system'
import type { UploadProps } from 'element-plus'
import { modifyMember } from '@/api/member'
import { ElMessage, UploadFile, UploadFiles } from 'element-plus'
import request from '@/utils/request'
import storage from '@/utils/storage'
@ -72,9 +68,9 @@ const updateNickname = reactive({
value: ''
})
const info = computed(() =>{
const info = computed(() => {
updateNickname.value = memberStore.info?.nickname;
if(memberStore.info) loading.value = false;
if (memberStore.info) loading.value = false;
return memberStore.info;
})
const appStore = useAppStore()
@ -89,13 +85,14 @@ const upload = computed(() => {
limit: 1,
headers,
onSuccess: (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
console.log('uploadFile',);
let img = uploadFile?.response?.data?.url;
if (response.code == 200) {
updateMember({
modifyMember({
field: 'headimg',
value: img
}).then(() => {
info.headimg = img
memberStore.info.headimg = img
})
} else {
uploadFile.status = 'fail'
@ -107,22 +104,23 @@ const upload = computed(() => {
//
const updateNicknameConfirm = () => {
if (!updateNickname.value) { ElMessage.error('会员昵称不能为空'); return }
if (!updateNickname.value) { ElMessage.error('会员昵称不能为空'); return }
updateMember({
field: 'nickname',
value: updateNickname.value
}).then(res => {
updateNickname.modal = false
})
modifyMember({
field: 'nickname',
value: updateNickname.value
}).then(res => {
updateNickname.modal = false
})
}
</script>
<style lang="scss" scoped>
.box-card{
.box-card {
border: none !important;
}
::v-deep .form-wrap .el-form-item{
::v-deep .form-wrap .el-form-item {
align-items: center;
}
</style>

View File

@ -14,14 +14,16 @@ interface FetchOptions {
onRequest?: (data: any) => void,
onResponse?: (data: any) => void,
onResponseError?: (data: any) => void,
showMessageConfig?: ShowMessageConfig
showMessageConfig?: ShowMessageConfig,
watch: boolean
}
class Http {
private options: FetchOptions = {
baseURL: '',
method: '',
headers: {}
headers: {},
watch: false
}
public constructor() {
@ -32,7 +34,7 @@ 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] = runtimeConfig.public.VITE_SITE_ID
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()
@ -46,7 +48,7 @@ class Http {
this.options.onResponse = ({ response, options }) => {
const { _data: data } = response
if (data.code == 200) {
if (data.code == 1) {
if (options.showMessageConfig.showSuccessMessage) ElMessage({ message: data.msg, type: 'success' })
} else {
if (options.showMessageConfig.showErrorMessage) ElMessage({ message: data.msg, type: 'error' })
@ -89,7 +91,7 @@ class Http {
const { data: { value }, error } = response
if (value) {
if (value.code == 200) {
if (value.code == 1) {
resolve(value)
} else {
reject(value)