Building a Cloud Notes Application with Vue.js and Spring Boot

Frontend Setup and Configuration

Vue.js Project Initialization

npm init vue@latest mycloud-notes
cd mycloud-notes
npm install

Essential Dependencies Installation

npm install element-plus axios sass vue-router@4 pinia pinia-persistedstate-plugin

Main Configuration File

// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createPersistedState } from 'pinia-persistedstate-plugin';
import ElementPlus from 'element-plus';
import locale from 'element-plus/dist/locale/zh-cn.js';
import 'element-plus/dist/index.css';
import router from '@/router';
import App from './App.vue';

const app = createApp(App);
const pinia = createPinia();
const persist = createPersistedState();

pinia.use(persist);
app.use(pinia);
app.use(ElementPlus, { locale });
app.use(router);
app.mount('#app');

Authentication System Implementation

Login Component Structure

<template>
  <el-row class="auth-container">
    <el-col class="background-section">
      <el-row class="brand-logo"></el-row>
      <el-row class="auth-form">
        <el-form v-if="isRegistrationMode" :model="authData" :rules="validationRules">
          <el-form-item>
            <h1>Create Account</h1>
          </el-form-item>
          <el-form-item prop="username">
            <el-input v-model="authData.username" placeholder="Enter username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input v-model="authData.password" type="password" placeholder="Enter password"></el-input>
          </el-form-item>
          <el-form-item prop="confirmPassword">
            <el-input v-model="authData.confirmPassword" type="password" placeholder="Confirm password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="handleRegistration">Register</el-button>
          </el-form-item>
        </el-form>
        
        <el-form v-else :model="authData" :rules="loginRules">
          <el-form-item>
            <h1>Sign In</h1>
          </el-form-item>
          <el-form-item prop="username">
            <el-input v-model="authData.username" placeholder="Enter username"></el-input>
          </el-form-item>
          <el-form-item prop="password">
            <el-input v-model="authData.password" type="password" placeholder="Enter password"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="handleLogin">Login</el-button>
          </el-form-item>
        </el-form>
      </el-row>
    </el-col>
  </el-row>
</template>

<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useTokenStore } from '@/stores/auth';
import { registerUser, authenticateUser } from '@/api/auth';

const router = useRouter();
const tokenStore = useTokenStore();
const isRegistrationMode = ref(false);

const authData = ref({
  username: '',
  password: '',
  confirmPassword: ''
});

const validationRules = {
  username: [
    { required: true, message: 'Username required', trigger: 'blur' },
    { min: 5, max: 16, message: '5-16 characters required', trigger: 'blur' }
  ],
  password: [
    { required: true, message: 'Password required', trigger: 'blur' },
    { min: 5, max: 16, message: '5-16 characters required', trigger: 'blur' }
  ],
  confirmPassword: [
    { validator: validatePasswordMatch, trigger: 'blur' }
  ]
};

function validatePasswordMatch(rule, value, callback) {
  if (value !== authData.value.password) {
    callback(new Error('Passwords must match'));
  } else {
    callback();
  }
}

async function handleRegistration() {
  await registerUser(authData.value);
  isRegistrationMode.value = false;
}

async function handleLogin() {
  const response = await authenticateUser(authData.value);
  tokenStore.setAuthToken(response.data);
  router.push('/');
}
</script>

API Service Implementation

// api/auth.js
import request from '@/utils/request';

export const registerUser = (userData) => {
  const params = new URLSearchParams();
  Object.keys(userData).forEach(key => {
    params.append(key, userData[key]);
  });
  return request.post('/user/register', params);
};

export const authenticateUser = (credentials) => {
  const params = new URLSearchParams();
  Object.keys(credentials).forEach(key => {
    params.append(key, credentials[key]);
  });
  return request.post('/user/login', params);
};

State Managmeent with Pinia

// stores/auth.js
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useTokenStore = defineStore('auth', () => {
  const authToken = ref('');
  
  const setAuthToken = (token) => {
    authToken.value = token;
  };
  
  const removeAuthToken = () => {
    authToken.value = '';
  };
  
  return { authToken, setAuthToken, removeAuthToken };
}, {
  persist: true
});

Application Routing Configuration

// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import LoginView from '@/views/Login.vue';
import MainLayout from '@/views/MainLayout.vue';
import ArticleCategories from '@/views/articles/Categories.vue';
import ArticleManagement from '@/views/articles/Management.vue';
import UserProfile from '@/views/user/Profile.vue';

const routes = [
  { path: '/login', component: LoginView },
  {
    path: '/',
    component: MainLayout,
    redirect: '/articles',
    children: [
      { path: '/articles/categories', component: ArticleCategories },
      { path: '/articles', component: ArticleManagement },
      { path: '/user/profile', component: UserProfile }
    ]
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

HTTP Request Configuration

// utils/request.js
import axios from 'axios';
import { ElMessage } from 'element-plus';
import router from '@/router';
import { useTokenStore } from '@/stores/auth';

const apiClient = axios.create({
  baseURL: '/api'
});

apiClient.interceptors.request.use(
  (config) => {
    const tokenStore = useTokenStore();
    if (tokenStore.authToken) {
      config.headers.Authorization = tokenStore.authToken;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

apiClient.interceptors.response.use(
  (response) => {
    if (response.data.code === 0) {
      return response.data;
    }
    ElMessage.error(response.data.message || 'Operation failed');
    return Promise.reject(response.data);
  },
  (error) => {
    if (error.response?.status === 401) {
      ElMessage.error('Authentication required');
      router.push('/login');
    } else {
      ElMessage.error('Service unavailable');
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Article Management Features

Category Management Component

<template>
  <el-card class="management-panel">
    <template #header>
      <div class="panel-header">
        <span>Article Categories</span>
        <el-button @click="showCategoryDialog">Add Category</el-button>
      </div>
    </template>
    
    <el-table :data="categories">
      <el-table-column label="ID" type="index" width="80" />
      <el-table-column label="Name" prop="categoryName" />
      <el-table-column label="Alias" prop="categoryAlias" />
      <el-table-column label="Actions" width="120">
        <template #default="{ row }">
          <el-button icon="Edit" @click="editCategory(row)" />
          <el-button icon="Delete" @click="removeCategory(row)" />
        </template>
      </el-table-column>
    </el-table>
    
    <el-dialog :title="dialogTitle" v-model="dialogVisible">
      <el-form :model="currentCategory" :rules="categoryRules">
        <el-form-item label="Category Name" prop="categoryName">
          <el-input v-model="currentCategory.categoryName" />
        </el-form-item>
        <el-form-item label="Category Alias" prop="categoryAlias">
          <el-input v-model="currentCategory.categoryAlias" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="dialogVisible = false">Cancel</el-button>
        <el-button @click="saveCategory">Confirm</el-button>
      </template>
    </el-dialog>
  </el-card>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { 
  fetchCategories, 
  createCategory, 
  updateCategory, 
  deleteCategory 
} from '@/api/articles';

const categories = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref('');
const currentCategory = ref({ id: null, categoryName: '', categoryAlias: '' });

const categoryRules = {
  categoryName: [{ required: true, message: 'Name required' }],
  categoryAlias: [{ required: true, message: 'Alias required' }]
};

onMounted(async () => {
  await loadCategories();
});

async function loadCategories() {
  const response = await fetchCategories();
  categories.value = response.data;
}

function showCategoryDialog() {
  dialogTitle.value = 'Add Category';
  currentCategory.value = { id: null, categoryName: '', categoryAlias: '' };
  dialogVisible.value = true;
}

function editCategory(category) {
  dialogTitle.value = 'Edit Category';
  currentCategory.value = { ...category };
  dialogVisible.value = true;
}

async function saveCategory() {
  if (currentCategory.value.id) {
    await updateCategory(currentCategory.value);
  } else {
    await createCategory(currentCategory.value);
  }
  dialogVisible.value = false;
  await loadCategories();
}

async function removeCategory(category) {
  try {
    await ElMessageBox.confirm('Confirm deletion?', 'Warning', {
      confirmButtonText: 'Confirm',
      cancelButtonText: 'Cancel',
      type: 'warning'
    });
    await deleteCategory(category.id);
    await loadCategories();
  } catch {
    ElMessage.info('Deletion cancelled');
  }
}
</script>

Article API Services

// api/articles.js
import request from '@/utils/request';

export const fetchCategories = () => request.get('/category');

export const createCategory = (categoryData) => 
  request.post('/category', categoryData);

export const updateCategory = (categoryData) => 
  request.put('/category', categoryData);

export const deleteCategory = (id) => 
  request.delete(`/category?id=${id}`);

export const fetchArticles = (params) => 
  request.get('/article', { params });

export const createArticle = (articleData) => 
  request.post('/article', articleData);

User Profile Management

Profile Update Component

<template>
  <el-card class="profile-panel">
    <template #header>
      <div class="panel-header">
        <span>User Profile</span>
      </div>
    </template>
    
    <el-form :model="userProfile" :rules="profileRules" label-width="100px">
      <el-form-item label="Username">
        <el-input v-model="userProfile.username" disabled />
      </el-form-item>
      <el-form-item label="Display Name" prop="nickname">
        <el-input v-model="userProfile.nickname" />
      </el-form-item>
      <el-form-item label="Email" prop="email">
        <el-input v-model="userProfile.email" />
      </el-form-item>
      <el-form-item>
        <el-button @click="updateProfile">Save Changes</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script setup>
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { useUserStore } from '@/stores/user';
import { fetchUserProfile, updateUserProfile } from '@/api/user';

const userStore = useUserStore();
const userProfile = ref({
  username: '',
  nickname: '',
  email: ''
});

const profileRules = {
  nickname: [
    { required: true, message: 'Display name required' },
    { pattern: /^\S{2,10}$/, message: '2-10 non-space characters' }
  ],
  email: [
    { required: true, message: 'Email required' },
    { type: 'email', message: 'Valid email required' }
  ]
};

onMounted(async () => {
  await loadUserProfile();
});

async function loadUserProfile() {
  const response = await fetchUserProfile();
  userProfile.value = response.data;
  userStore.setUserInfo(response.data);
}

async function updateProfile() {
  await updateUserProfile(userProfile.value);
  userStore.setUserInfo(userProfile.value);
  ElMessage.success('Profile updated');
}
</script>

User Store Implementation

// stores/user.js
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useUserStore = defineStore('user', () => {
  const userInfo = ref({});
  
  const setUserInfo = (info) => {
    userInfo.value = info;
  };
  
  const clearUserInfo = () => {
    userInfo.value = {};
  };
  
  return { userInfo, setUserInfo, clearUserInfo };
}, {
  persist: true
});

Tags: Vue.js Spring Boot Full-Stack Development Authentication State Management

Posted on Sat, 09 May 2026 02:20:07 +0000 by systemick