wip
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -80,6 +80,7 @@ declare module 'vue' {
|
||||
IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default']
|
||||
IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default']
|
||||
IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default']
|
||||
IconFluentDocument20Regular: typeof import('~icons/fluent/document20-regular')['default']
|
||||
IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default']
|
||||
IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default']
|
||||
IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default']
|
||||
|
||||
@@ -1,21 +1,263 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
// 资源分类
|
||||
const categories = ref([
|
||||
{
|
||||
id: 'new-concept',
|
||||
name: '新概念英语',
|
||||
icon: '📚',
|
||||
description: '经典英语教材,适合系统学习',
|
||||
resources: [
|
||||
{
|
||||
name: '新概念英语第一册',
|
||||
description: '适合英语初学者',
|
||||
difficulty: '入门',
|
||||
link: 'https://pan.quark.cn/s/92a317cf1a16',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第二册',
|
||||
description: '基础英语学习,巩固语法和词汇',
|
||||
difficulty: '基础',
|
||||
link: 'https://pan.quark.cn/s/1ee9c8a7e8e2',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第三册',
|
||||
description: '提高英语水平,增强阅读能力',
|
||||
difficulty: '进阶',
|
||||
link: 'https://pan.quark.cn/s/b35c2859812a',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第四册',
|
||||
description: '高级英语学习,提升综合能力',
|
||||
difficulty: '高级',
|
||||
link: 'https://pan.quark.cn/s/a56713cafbc5',
|
||||
},
|
||||
{
|
||||
name: '新概念英青少年版',
|
||||
description: '儿童读物',
|
||||
difficulty: '7岁至14岁',
|
||||
link: 'https://pan.quark.cn/s/9de8d7967de2',
|
||||
},
|
||||
{
|
||||
name: '新概念英语1-4 教材高清PDF',
|
||||
description: '仅 1-4 册的教材高清PDF',
|
||||
difficulty: '',
|
||||
link: 'https://pan.quark.cn/s/ec49145d6b00',
|
||||
},
|
||||
{
|
||||
name: '新概念讲解视频',
|
||||
description: '包含了 N 家机构/个人的讲解视频',
|
||||
difficulty: '',
|
||||
link: 'https://pan.quark.cn/s/09e98acd55b4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'exam',
|
||||
name: '英语相关电视/电影',
|
||||
icon: '🎯',
|
||||
description: '雅思、托福等考试备考资料',
|
||||
resources: [
|
||||
{
|
||||
name: '老友记',
|
||||
description: '',
|
||||
difficulty: '经典',
|
||||
link: 'https://pan.quark.cn/s/674834e7a5b1',
|
||||
},
|
||||
{
|
||||
name: '生活大爆炸',
|
||||
description: '',
|
||||
difficulty: '经典',
|
||||
link: 'https://pan.quark.cn/s/0539c10704ba',
|
||||
},
|
||||
{
|
||||
name: '是大臣/是首相',
|
||||
description: '',
|
||||
difficulty: '经典',
|
||||
link: 'https://pan.quark.cn/s/316323ce51d5',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grammar',
|
||||
name: '语法学习',
|
||||
icon: '📝',
|
||||
description: '系统性学习英语语法',
|
||||
resources: [
|
||||
{
|
||||
name: '剑桥中级英语语法',
|
||||
description: '清晰讲解语法点,配有大量练习',
|
||||
difficulty: '中级',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
{
|
||||
name: 'English Grammar in Use',
|
||||
description: '经典语法教材,适合自学',
|
||||
difficulty: '中级',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
{
|
||||
name: "Murphy's English Grammar",
|
||||
description: '系统讲解英语语法,适合各类学习者',
|
||||
difficulty: '全级别',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'listening',
|
||||
name: '听力训练',
|
||||
icon: '🎧',
|
||||
description: '提升英语听力水平',
|
||||
resources: [
|
||||
{
|
||||
name: 'VOA慢速英语合集',
|
||||
description: '新闻类听力材料,语速适中,内容丰富',
|
||||
difficulty: '中级',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
{
|
||||
name: 'BBC Learning English',
|
||||
description: 'BBC官方英语学习资源,涵盖多方面内容',
|
||||
difficulty: '中高级',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
{
|
||||
name: 'TED演讲精选',
|
||||
description: '高质量演讲,锻炼听力同时开拓视野',
|
||||
difficulty: '中高级',
|
||||
link: 'https://pan.baidu.com/s/xxx',
|
||||
},
|
||||
{
|
||||
name: '哈弗演讲',
|
||||
description: '高质量演讲,锻炼听力同时开拓视野',
|
||||
difficulty: '中高级',
|
||||
link: 'https://pan.quark.cn/s/f2bfa8a50d25',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的分类
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
// 筛选后的资源
|
||||
const filteredResources = computed(() => {
|
||||
if (selectedCategory.value === 'all') {
|
||||
return categories.value
|
||||
}
|
||||
return categories.value.filter(cat => cat.id === selectedCategory.value)
|
||||
})
|
||||
|
||||
// 跳转到网盘链接
|
||||
const openLink = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 根据难度获取对应的样式类
|
||||
const getDifficultyClass = (difficulty: string) => {
|
||||
switch (difficulty) {
|
||||
case '入门':
|
||||
return 'bg-green-500'
|
||||
case '基础':
|
||||
return 'bg-blue-500'
|
||||
case '中级':
|
||||
return 'bg-purple-500'
|
||||
case '进阶':
|
||||
return 'bg-amber-500'
|
||||
case '高级':
|
||||
return 'bg-red-500'
|
||||
case '全级别':
|
||||
return 'bg-gray-500'
|
||||
default:
|
||||
return 'bg-blue-500'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="center">
|
||||
<div class="card qa w-2/3">
|
||||
<div class="font-bold text-2xl mb-6">分享个人收藏的一些学习资料</div>
|
||||
<div class="list">
|
||||
<div class="title">新概念相关</div>
|
||||
<div class="line"></div>
|
||||
<div class="flex flex-col items-center justify-center px-4 py-8 max-w-7xl mx-auto">
|
||||
<!-- 页面标题 -->
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold mb-4">📚 英语学习资源分享</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
以下是我整理的个人收藏的优质英语学习资源,希望对大家有所帮助!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<div
|
||||
class="flex flex-wrap justify-center gap-2 mb-8 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md w-full"
|
||||
>
|
||||
<BaseButton
|
||||
:type="selectedCategory === 'all' ? 'primary' : 'info'"
|
||||
@click="selectedCategory = 'all'"
|
||||
>
|
||||
全部资源
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:type="selectedCategory === category.id ? 'primary' : 'info'"
|
||||
@click="selectedCategory = category.id"
|
||||
>
|
||||
{{ category.icon }} {{ category.name }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div class="w-full">
|
||||
<div v-for="category in filteredResources" :key="category.id" class="mb-12">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold mb-2">{{ category.icon }} {{ category.name }}</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">{{ category.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<div
|
||||
v-for="resource in category.resources"
|
||||
:key="resource.name"
|
||||
class="card-white mb-0 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 flex flex-col justify-between"
|
||||
>
|
||||
<div class="">
|
||||
<div class="text-xl font-semibold mb-2 text-gray-800 dark:text-gray-100">
|
||||
{{ resource.name }}
|
||||
</div>
|
||||
<p>
|
||||
<span
|
||||
v-if="resource.difficulty"
|
||||
class="mr-2 inline-block px-3 py-1 rounded-full text-xs font-medium text-white"
|
||||
:class="getDifficultyClass(resource.difficulty)"
|
||||
>
|
||||
{{ resource.difficulty }}
|
||||
</span>
|
||||
<span class=" text-gray-600 dark:text-gray-300 mb-4">{{
|
||||
resource.description
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<BaseButton type="primary" @click="openLink(resource.link)"> 打开链接 </BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面底部 -->
|
||||
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
|
||||
<h3 class="text-xl font-bold mb-4">💡 温馨提示</h3>
|
||||
<ul class="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-300">
|
||||
<li>所有资源均来自互联网收集,仅供学习交流使用</li>
|
||||
<li>如果链接失效,请及时告知,我会尽快更新</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -1,111 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ShortcutKey} from "@/types/types.ts";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import { jump2Feedback } from "@/utils";
|
||||
import { ShortcutKey } from '@/types/types.ts'
|
||||
import Logo from '@/components/Logo.vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useTheme from '@/hooks/theme.ts'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import { jump2Feedback } from '@/utils'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const router = useRouter()
|
||||
const {toggleTheme, getTheme} = useTheme()
|
||||
const { toggleTheme, getTheme } = useTheme()
|
||||
|
||||
//首页为了seo被剥离出去了,现在是一个静态页面,用nginx 重定向控制对应的跳转
|
||||
function goHome() {
|
||||
window.location.href = '/';
|
||||
window.location.href = '/'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout anim">
|
||||
<!-- 第一个aside 占位用-->
|
||||
<div class="aside space" :class="{'expand':settingStore.sideExpand}"></div>
|
||||
<div class="aside anim fixed" :class="{'expand':settingStore.sideExpand}">
|
||||
<div class="aside space" :class="{ expand: settingStore.sideExpand }"></div>
|
||||
<div class="aside anim fixed" :class="{ expand: settingStore.sideExpand }">
|
||||
<div class="top">
|
||||
<Logo v-if="settingStore.sideExpand"/>
|
||||
<Logo v-if="settingStore.sideExpand" />
|
||||
<div class="row" @click="goHome">
|
||||
<IconFluentHome20Regular/>
|
||||
<IconFluentHome20Regular />
|
||||
<span v-if="settingStore.sideExpand">主页</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/words')">
|
||||
<IconFluentTextUnderlineDouble20Regular/>
|
||||
<IconFluentTextUnderlineDouble20Regular />
|
||||
<span v-if="settingStore.sideExpand">单词</span>
|
||||
</div>
|
||||
<div id="article" class="row" @click="router.push('/articles')">
|
||||
<div id="article" class="row" @click="router.push('/articles')">
|
||||
<!-- <IconPhArticleNyTimes/>-->
|
||||
<IconFluentBookLetter20Regular/>
|
||||
<IconFluentBookLetter20Regular />
|
||||
<span v-if="settingStore.sideExpand">文章</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/setting')">
|
||||
<IconFluentSettings20Regular/>
|
||||
<IconFluentSettings20Regular />
|
||||
<span v-if="settingStore.sideExpand">设置</span>
|
||||
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
|
||||
<div
|
||||
class="red-point"
|
||||
:class="!settingStore.sideExpand && 'top-1 right-0'"
|
||||
v-if="runtimeStore.isNew"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/feedback')">
|
||||
<IconFluentCommentEdit20Regular/>
|
||||
<IconFluentCommentEdit20Regular />
|
||||
<span v-if="settingStore.sideExpand">反馈</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/qa')">
|
||||
<IconFluentQuestionCircle20Regular/>
|
||||
<IconFluentQuestionCircle20Regular />
|
||||
<span v-if="settingStore.sideExpand">帮助</span>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/doc')">-->
|
||||
<!-- <IconFluentDocument20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">资料</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
<div class="row" @click="router.push('/doc')">
|
||||
<IconFluentDocument20Regular />
|
||||
<span v-if="settingStore.sideExpand">资料</span>
|
||||
</div>
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="bottom flex justify-evenly ">
|
||||
<BaseIcon
|
||||
@click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand"/>
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else/>
|
||||
<div class="bottom flex justify-evenly">
|
||||
<BaseIcon @click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand" />
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
v-if="settingStore.sideExpand"
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'"/>
|
||||
<IconFluentWeatherSunny16Regular v-else/>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'" />
|
||||
<IconFluentWeatherSunny16Regular v-else />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 移动端顶部菜单栏 -->
|
||||
<div class="mobile-top-nav" :class="{'collapsed': settingStore.mobileNavCollapsed}">
|
||||
<div class="mobile-top-nav" :class="{ collapsed: settingStore.mobileNavCollapsed }">
|
||||
<div class="nav-items">
|
||||
<div class="nav-item" @click="router.push('/')" :class="{'active': $route.path === '/'}">
|
||||
<IconFluentHome20Regular/>
|
||||
<div class="nav-item" @click="router.push('/')" :class="{ active: $route.path === '/' }">
|
||||
<IconFluentHome20Regular />
|
||||
<span>主页</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="router.push('/words')" :class="{'active': $route.path.includes('/words')}">
|
||||
<IconFluentTextUnderlineDouble20Regular/>
|
||||
<div
|
||||
class="nav-item"
|
||||
@click="router.push('/words')"
|
||||
:class="{ active: $route.path.includes('/words') }"
|
||||
>
|
||||
<IconFluentTextUnderlineDouble20Regular />
|
||||
<span>单词</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="router.push('/articles')" :class="{'active': $route.path.includes('/articles')}">
|
||||
<IconFluentBookLetter20Regular/>
|
||||
<div
|
||||
class="nav-item"
|
||||
@click="router.push('/articles')"
|
||||
:class="{ active: $route.path.includes('/articles') }"
|
||||
>
|
||||
<IconFluentBookLetter20Regular />
|
||||
<span>文章</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="router.push('/setting')" :class="{'active': $route.path === '/setting'}">
|
||||
<IconFluentSettings20Regular/>
|
||||
<div
|
||||
class="nav-item"
|
||||
@click="router.push('/setting')"
|
||||
:class="{ active: $route.path === '/setting' }"
|
||||
>
|
||||
<IconFluentSettings20Regular />
|
||||
<span>设置</span>
|
||||
<div class="red-point" v-if="runtimeStore.isNew"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-toggle" @click="settingStore.mobileNavCollapsed = !settingStore.mobileNavCollapsed">
|
||||
<IconFluentChevronDown20Filled v-if="!settingStore.mobileNavCollapsed"/>
|
||||
<IconFluentChevronUp20Filled v-else/>
|
||||
<div
|
||||
class="nav-toggle"
|
||||
@click="settingStore.mobileNavCollapsed = !settingStore.mobileNavCollapsed"
|
||||
>
|
||||
<IconFluentChevronDown20Filled v-if="!settingStore.mobileNavCollapsed" />
|
||||
<IconFluentChevronUp20Filled v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-1 z-1 relative main-content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
@@ -134,7 +151,7 @@ function goHome() {
|
||||
|
||||
.row {
|
||||
@apply cursor-pointer rounded-md text p-2 my-2 flex items-center gap-2 relative shrink-0;
|
||||
transition: all .5s;
|
||||
transition: all 0.5s;
|
||||
|
||||
&:hover {
|
||||
background: var(--btn-primary);
|
||||
@@ -167,12 +184,12 @@ function goHome() {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
|
||||
.nav-items {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -184,29 +201,30 @@ function goHome() {
|
||||
min-width: 44px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
|
||||
svg {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-main-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
&.active {
|
||||
svg, span {
|
||||
svg,
|
||||
span {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
.red-point {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
@@ -218,7 +236,7 @@ function goHome() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.nav-toggle {
|
||||
position: absolute;
|
||||
bottom: -1.5rem;
|
||||
@@ -231,20 +249,20 @@ function goHome() {
|
||||
padding: 0.3rem 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
|
||||
svg {
|
||||
font-size: 1rem;
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.collapsed {
|
||||
transform: translateY(calc(-100% + 1.5rem));
|
||||
|
||||
|
||||
.nav-items {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -264,11 +282,11 @@ function goHome() {
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.aside.space {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
|
||||
Reference in New Issue
Block a user