vue使用语法糖
This commit is contained in:
@@ -1,4 +1,53 @@
|
|||||||
<!-- src/App.vue -->
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const isLoggedIn = ref(false)
|
||||||
|
const isLoading = ref(true)
|
||||||
|
|
||||||
|
// 检查登录状态
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
isLoggedIn.value = !!localStorage.getItem('user-token')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('user-token')
|
||||||
|
isLoggedIn.value = false
|
||||||
|
|
||||||
|
if (router.currentRoute.value.meta.requiresAuth) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由变化时检查登录状态
|
||||||
|
watch(() => router.currentRoute.value, () => {
|
||||||
|
checkLoginStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件创建时检查登录状态
|
||||||
|
checkLoginStatus()
|
||||||
|
|
||||||
|
// 确保主题和样式已完全加载后再显示内容
|
||||||
|
nextTick(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
}, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由变化时的加载状态
|
||||||
|
router.beforeEach(() => {
|
||||||
|
isLoading.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
isLoading.value = false
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-app>
|
<v-app>
|
||||||
<!-- 添加加载指示器 -->
|
<!-- 添加加载指示器 -->
|
||||||
@@ -15,61 +64,6 @@
|
|||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isLoggedIn: false,
|
|
||||||
isLoading: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.checkLoginStatus()
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// 确保主题和样式已完全加载后再显示内容
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// 短暂延迟确保DOM完全渲染
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
}, 200)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加路由变化时的加载状态
|
|
||||||
this.$router.beforeEach((to, from, next) => {
|
|
||||||
this.isLoading = true
|
|
||||||
next()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$router.afterEach(() => {
|
|
||||||
// 路由加载完成后,短暂延迟以确保组件已渲染
|
|
||||||
setTimeout(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
checkLoginStatus() {
|
|
||||||
this.isLoggedIn = !!localStorage.getItem('user-token')
|
|
||||||
},
|
|
||||||
logout() {
|
|
||||||
localStorage.removeItem('user-token')
|
|
||||||
this.isLoggedIn = false
|
|
||||||
|
|
||||||
// 如果在管理页面退出,则重定向到首页
|
|
||||||
if (this.$route.meta.requiresAuth) {
|
|
||||||
this.$router.push('/')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route() {
|
|
||||||
this.checkLoginStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
|
|
||||||
|
<script setup>
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Vuetify Documentation',
|
||||||
|
icon: `$vuetify`,
|
||||||
|
href: 'https://vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Support',
|
||||||
|
icon: 'mdi-shield-star-outline',
|
||||||
|
href: 'https://support.vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify X',
|
||||||
|
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
|
||||||
|
href: 'https://x.com/vuetifyjs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify GitHub',
|
||||||
|
icon: `mdi-github`,
|
||||||
|
href: 'https://github.com/vuetifyjs/vuetify',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Discord',
|
||||||
|
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
|
||||||
|
href: 'https://community.vuetifyjs.com/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Vuetify Reddit',
|
||||||
|
icon: `mdi-reddit`,
|
||||||
|
href: 'https://reddit.com/r/vuetifyjs',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-footer
|
<v-footer
|
||||||
app
|
app
|
||||||
@@ -36,41 +72,6 @@
|
|||||||
</v-footer>
|
</v-footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const items = [
|
|
||||||
{
|
|
||||||
title: 'Vuetify Documentation',
|
|
||||||
icon: `$vuetify`,
|
|
||||||
href: 'https://vuetifyjs.com/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vuetify Support',
|
|
||||||
icon: 'mdi-shield-star-outline',
|
|
||||||
href: 'https://support.vuetifyjs.com/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vuetify X',
|
|
||||||
icon: ['M2.04875 3.00002L9.77052 13.3248L1.99998 21.7192H3.74882L10.5519 14.3697L16.0486 21.7192H22L13.8437 10.8137L21.0765 3.00002H19.3277L13.0624 9.76874L8.0001 3.00002H2.04875ZM4.62054 4.28821H7.35461L19.4278 20.4308H16.6937L4.62054 4.28821Z'],
|
|
||||||
href: 'https://x.com/vuetifyjs',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vuetify GitHub',
|
|
||||||
icon: `mdi-github`,
|
|
||||||
href: 'https://github.com/vuetifyjs/vuetify',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vuetify Discord',
|
|
||||||
icon: ['M22,24L16.75,19L17.38,21H4.5A2.5,2.5 0 0,1 2,18.5V3.5A2.5,2.5 0 0,1 4.5,1H19.5A2.5,2.5 0 0,1 22,3.5V24M12,6.8C9.32,6.8 7.44,7.95 7.44,7.95C8.47,7.03 10.27,6.5 10.27,6.5L10.1,6.33C8.41,6.36 6.88,7.53 6.88,7.53C5.16,11.12 5.27,14.22 5.27,14.22C6.67,16.03 8.75,15.9 8.75,15.9L9.46,15C8.21,14.73 7.42,13.62 7.42,13.62C7.42,13.62 9.3,14.9 12,14.9C14.7,14.9 16.58,13.62 16.58,13.62C16.58,13.62 15.79,14.73 14.54,15L15.25,15.9C15.25,15.9 17.33,16.03 18.73,14.22C18.73,14.22 18.84,11.12 17.12,7.53C17.12,7.53 15.59,6.36 13.9,6.33L13.73,6.5C13.73,6.5 15.53,7.03 16.56,7.95C16.56,7.95 14.68,6.8 12,6.8M9.93,10.59C10.58,10.59 11.11,11.16 11.1,11.86C11.1,12.55 10.58,13.13 9.93,13.13C9.29,13.13 8.77,12.55 8.77,11.86C8.77,11.16 9.28,10.59 9.93,10.59M14.1,10.59C14.75,10.59 15.27,11.16 15.27,11.86C15.27,12.55 14.75,13.13 14.1,13.13C13.46,13.13 12.94,12.55 12.94,11.86C12.94,11.16 13.45,10.59 14.1,10.59Z'],
|
|
||||||
href: 'https://community.vuetifyjs.com/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Vuetify Reddit',
|
|
||||||
icon: `mdi-reddit`,
|
|
||||||
href: 'https://reddit.com/r/vuetifyjs',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass">
|
||||||
.social-link :deep(.v-icon)
|
.social-link :deep(.v-icon)
|
||||||
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
|
color: rgba(var(--v-theme-on-background), var(--v-disabled-opacity))
|
||||||
|
|||||||
@@ -1,3 +1,75 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import storageService from '@/services/storage_service'
|
||||||
|
|
||||||
|
const cachedItemsCount = ref(0)
|
||||||
|
const lastCleanTime = ref(null)
|
||||||
|
const cacheMessage = ref('')
|
||||||
|
const cacheMessageType = ref('info')
|
||||||
|
const clearing = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期显示
|
||||||
|
* @param {Date} date - 日期对象
|
||||||
|
* @returns {string} 格式化的日期字符串
|
||||||
|
*/
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return ''
|
||||||
|
try {
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
}).format(date)
|
||||||
|
} catch (e) {
|
||||||
|
return date.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新缓存信息
|
||||||
|
*/
|
||||||
|
const updateCacheInfo = () => {
|
||||||
|
try {
|
||||||
|
const allItems = storageService.getAllCachedItems()
|
||||||
|
cachedItemsCount.value = Object.keys(allItems).length
|
||||||
|
|
||||||
|
const cleanTimeStr = localStorage.getItem('findreve-last-clean-time')
|
||||||
|
lastCleanTime.value = cleanTimeStr ? new Date(parseInt(cleanTimeStr)) : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存信息失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有缓存
|
||||||
|
*/
|
||||||
|
const clearCache = async () => {
|
||||||
|
clearing.value = true
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 600))
|
||||||
|
storageService.clearAllCache()
|
||||||
|
localStorage.setItem('findreve-last-clean-time', Date.now().toString())
|
||||||
|
updateCacheInfo()
|
||||||
|
cacheMessage.value = '缓存已成功清除'
|
||||||
|
cacheMessageType.value = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除缓存失败', error)
|
||||||
|
cacheMessage.value = '清除缓存失败: ' + error.message
|
||||||
|
cacheMessageType.value = 'error'
|
||||||
|
} finally {
|
||||||
|
clearing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateCacheInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-card class="my-3">
|
<v-card class="my-3">
|
||||||
<v-card-title class="d-flex align-center">
|
<v-card-title class="d-flex align-center">
|
||||||
@@ -40,97 +112,4 @@
|
|||||||
</v-alert>
|
</v-alert>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 缓存状态组件
|
|
||||||
*
|
|
||||||
* 显示当前本地缓存的状态信息,支持清除缓存
|
|
||||||
*/
|
|
||||||
import storageService from '@/services/storage_service';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'CacheStatus',
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cachedItemsCount: 0,
|
|
||||||
lastCleanTime: null,
|
|
||||||
cacheMessage: '',
|
|
||||||
cacheMessageType: 'info',
|
|
||||||
clearing: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.updateCacheInfo();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 更新缓存信息
|
|
||||||
*/
|
|
||||||
updateCacheInfo() {
|
|
||||||
try {
|
|
||||||
const allItems = storageService.getAllCachedItems();
|
|
||||||
this.cachedItemsCount = Object.keys(allItems).length;
|
|
||||||
|
|
||||||
// 获取上次清理时间(这里需要在storage_service中添加记录)
|
|
||||||
const cleanTimeStr = localStorage.getItem('findreve-last-clean-time');
|
|
||||||
this.lastCleanTime = cleanTimeStr ? new Date(parseInt(cleanTimeStr)) : null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取缓存信息失败', error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除所有缓存
|
|
||||||
*/
|
|
||||||
async clearCache() {
|
|
||||||
this.clearing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 小延迟以显示加载效果
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 600));
|
|
||||||
|
|
||||||
storageService.clearAllCache();
|
|
||||||
localStorage.setItem('findreve-last-clean-time', Date.now().toString());
|
|
||||||
|
|
||||||
this.updateCacheInfo();
|
|
||||||
this.cacheMessage = '缓存已成功清除';
|
|
||||||
this.cacheMessageType = 'success';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('清除缓存失败', error);
|
|
||||||
this.cacheMessage = '清除缓存失败: ' + error.message;
|
|
||||||
this.cacheMessageType = 'error';
|
|
||||||
} finally {
|
|
||||||
this.clearing = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化日期显示
|
|
||||||
*
|
|
||||||
* @param {Date} date - 日期对象
|
|
||||||
* @returns {string} 格式化的日期字符串
|
|
||||||
*/
|
|
||||||
formatDate(date) {
|
|
||||||
if (!date) return '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit'
|
|
||||||
}).format(date);
|
|
||||||
} catch (e) {
|
|
||||||
return date.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
const systemInfo = {
|
||||||
|
version: '2.0.0 Alpha',
|
||||||
|
releaseDate: '2025-07-15',
|
||||||
|
framework: 'FastAPI + Vue'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-h4 mb-4">关于 Findreve</h2>
|
<h2 class="text-h4 mb-4">关于 Findreve</h2>
|
||||||
@@ -61,19 +70,4 @@
|
|||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'AboutSystemComponent',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
systemInfo: {
|
|
||||||
version: '2.0.0 Alpha',
|
|
||||||
releaseDate: '2025-07-15',
|
|
||||||
framework: 'FastAPI + Vue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 仪表盘数据
|
||||||
|
const itemStats = ref({
|
||||||
|
total: 0,
|
||||||
|
normal: 0,
|
||||||
|
lost: 0,
|
||||||
|
scans: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新统计信息
|
||||||
|
*/
|
||||||
|
const updateStats = (items) => {
|
||||||
|
itemStats.value.total = items.length
|
||||||
|
itemStats.value.normal = items.filter(item => item.status === 'ok').length
|
||||||
|
itemStats.value.lost = items.filter(item => item.status === 'lost').length
|
||||||
|
itemStats.value.scans = items.reduce((sum, item) => sum + (item.views || 0), 0)
|
||||||
|
if (itemStats.value.scans === 0) itemStats.value.scans = Math.floor(Math.random() * 100) + 50
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算百分比
|
||||||
|
*/
|
||||||
|
const getPercentage = (type) => {
|
||||||
|
if (itemStats.value.total === 0) return 0
|
||||||
|
return Math.round((itemStats.value[type] / itemStats.value.total) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.items, (newItems) => {
|
||||||
|
updateStats(newItems)
|
||||||
|
}, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-h4 mb-4">仪表盘</h2>
|
<h2 class="text-h4 mb-4">仪表盘</h2>
|
||||||
@@ -40,74 +82,4 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 仪表盘组件
|
|
||||||
*
|
|
||||||
* 显示物品统计信息和系统概览
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
name: 'DashboardComponent',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// 仪表盘数据
|
|
||||||
itemStats: {
|
|
||||||
total: 0,
|
|
||||||
normal: 0,
|
|
||||||
lost: 0,
|
|
||||||
scans: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
items: {
|
|
||||||
handler(newItems) {
|
|
||||||
this.updateStats(newItems);
|
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 更新统计信息
|
|
||||||
*
|
|
||||||
* @param {Array} items - 物品列表
|
|
||||||
*/
|
|
||||||
updateStats(items) {
|
|
||||||
// 计算物品总数
|
|
||||||
this.itemStats.total = items.length;
|
|
||||||
|
|
||||||
// 计算正常和丢失物品数量
|
|
||||||
this.itemStats.normal = items.filter(item => item.status === 'ok').length;
|
|
||||||
this.itemStats.lost = items.filter(item => item.status === 'lost').length;
|
|
||||||
|
|
||||||
// 假设扫描次数是从物品中累计的一个属性
|
|
||||||
this.itemStats.scans = items.reduce((sum, item) => sum + (item.views || 0), 0);
|
|
||||||
if (this.itemStats.scans === 0) this.itemStats.scans = Math.floor(Math.random() * 100) + 50; // 示例数据
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算百分比
|
|
||||||
*
|
|
||||||
* @param {string} type - 物品类型(normal或lost)
|
|
||||||
* @returns {number} 百分比值
|
|
||||||
*/
|
|
||||||
getPercentage(type) {
|
|
||||||
if (this.itemStats.total === 0) return 0;
|
|
||||||
return Math.round((this.itemStats[type] / this.itemStats.total) * 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,3 +1,254 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import apiService from '@/services/api_service'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
// 界面控制
|
||||||
|
const loading = ref(false)
|
||||||
|
const search = ref('')
|
||||||
|
const statusFilter = ref('all')
|
||||||
|
|
||||||
|
// 物品管理
|
||||||
|
const editItem = ref({
|
||||||
|
id: null,
|
||||||
|
key: '',
|
||||||
|
name: '',
|
||||||
|
icon: '',
|
||||||
|
phone: '',
|
||||||
|
status: 'ok',
|
||||||
|
context: ''
|
||||||
|
})
|
||||||
|
const defaultItem = {
|
||||||
|
id: null,
|
||||||
|
key: '',
|
||||||
|
name: '',
|
||||||
|
icon: '',
|
||||||
|
phone: '',
|
||||||
|
status: 'ok',
|
||||||
|
context: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框控制
|
||||||
|
const itemDialog = ref(false)
|
||||||
|
const deleteDialog = ref(false)
|
||||||
|
const qrDialog = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const deleting = ref(false)
|
||||||
|
const formValid = ref(false)
|
||||||
|
|
||||||
|
// 选中的物品和删除项
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
const deleteItem = ref(null)
|
||||||
|
|
||||||
|
// 表格配置
|
||||||
|
const headers = [
|
||||||
|
{ title: 'ID', key: 'id', sortable: true },
|
||||||
|
{ title: '物品名称', key: 'name', sortable: true },
|
||||||
|
{ title: '标识码', key: 'key', sortable: true },
|
||||||
|
{ title: '状态', key: 'status', sortable: true },
|
||||||
|
{ title: '创建时间', key: 'created_at', sortable: true },
|
||||||
|
{ title: '操作', key: 'actions', sortable: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: '全部状态', value: 'all' },
|
||||||
|
{ title: '正常', value: 'ok' },
|
||||||
|
{ title: '丢失', value: 'lost' }
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤后的物品列表
|
||||||
|
*/
|
||||||
|
const filteredItems = computed(() => {
|
||||||
|
let result = [...props.items]
|
||||||
|
if (statusFilter.value !== 'all') {
|
||||||
|
result = result.filter(item => item.status === statusFilter.value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开物品对话框
|
||||||
|
*/
|
||||||
|
const openItemDialog = (item = null) => {
|
||||||
|
editItem.value = item ? JSON.parse(JSON.stringify(item)) : JSON.parse(JSON.stringify(defaultItem))
|
||||||
|
if (!item) {
|
||||||
|
editItem.value.key = generateRandomKey()
|
||||||
|
}
|
||||||
|
itemDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存物品
|
||||||
|
*/
|
||||||
|
const saveItem = async () => {
|
||||||
|
if (!formValid.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
saving.value = true
|
||||||
|
let data
|
||||||
|
|
||||||
|
if (editItem.value.id) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
const { id, key, name, icon, phone, status, context } = editItem.value
|
||||||
|
|
||||||
|
params.append('id', id)
|
||||||
|
params.append('key', key)
|
||||||
|
params.append('name', name)
|
||||||
|
params.append('icon', icon || '')
|
||||||
|
params.append('phone', phone)
|
||||||
|
params.append('status', status)
|
||||||
|
|
||||||
|
if (status === 'lost' && context) {
|
||||||
|
params.append('context', context)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = await apiService.patch(`/api/admin/items?${params.toString()}`, '')
|
||||||
|
} else {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
const { key, name, icon, phone } = editItem.value
|
||||||
|
|
||||||
|
params.append('key', key)
|
||||||
|
params.append('name', name)
|
||||||
|
params.append('icon', icon || '')
|
||||||
|
params.append('phone', phone)
|
||||||
|
|
||||||
|
data = await apiService.post(`/api/admin/items?${params.toString()}`, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.code !== 0) {
|
||||||
|
throw new Error(data.msg || '保存物品失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
itemDialog.value = false
|
||||||
|
emit('refresh')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存物品错误:', error)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认删除物品
|
||||||
|
*/
|
||||||
|
const confirmDelete = (item) => {
|
||||||
|
deleteItem.value = item
|
||||||
|
deleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认删除物品
|
||||||
|
*/
|
||||||
|
const deleteItemConfirm = async () => {
|
||||||
|
if (!deleteItem.value?.id) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
deleting.value = true
|
||||||
|
const data = await apiService.delete(`/api/admin/items?id=${encodeURIComponent(deleteItem.value.id)}`)
|
||||||
|
|
||||||
|
if (data.code !== 0) {
|
||||||
|
throw new Error(data.msg || '删除物品失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDialog.value = false
|
||||||
|
emit('refresh')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除物品错误:', error)
|
||||||
|
} finally {
|
||||||
|
deleting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示二维码
|
||||||
|
*/
|
||||||
|
const showQRCode = (item) => {
|
||||||
|
selectedItem.value = item
|
||||||
|
qrDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取二维码URL
|
||||||
|
*/
|
||||||
|
const getQRCodeUrl = (key) => {
|
||||||
|
const currentUrl = window.location.origin
|
||||||
|
const foundUrl = `${currentUrl}/found?key=${encodeURIComponent(key)}`
|
||||||
|
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(foundUrl)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机标识码
|
||||||
|
*/
|
||||||
|
const generateRandomKey = () => {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
|
let result = ''
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的颜色
|
||||||
|
*/
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
ok: "success",
|
||||||
|
lost: "error",
|
||||||
|
default: "grey"
|
||||||
|
}
|
||||||
|
return statusMap[status] || statusMap.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的文本
|
||||||
|
*/
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
ok: "正常",
|
||||||
|
lost: "丢失",
|
||||||
|
default: "未知"
|
||||||
|
}
|
||||||
|
return statusMap[status] || statusMap.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期显示
|
||||||
|
*/
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return "未知时间"
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(date)
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有筛选条件
|
||||||
|
*/
|
||||||
|
const resetFilters = () => {
|
||||||
|
search.value = ''
|
||||||
|
statusFilter.value = 'all'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="d-flex justify-space-between align-center mb-4">
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
@@ -260,345 +511,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import apiService from '@/services/api_service';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ItemsManagementComponent',
|
|
||||||
|
|
||||||
props: {
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// 界面控制
|
|
||||||
loading: false,
|
|
||||||
search: '',
|
|
||||||
statusFilter: 'all',
|
|
||||||
|
|
||||||
// 物品管理
|
|
||||||
editItem: {
|
|
||||||
id: null,
|
|
||||||
key: '',
|
|
||||||
name: '',
|
|
||||||
icon: '',
|
|
||||||
phone: '',
|
|
||||||
status: 'ok',
|
|
||||||
context: ''
|
|
||||||
},
|
|
||||||
defaultItem: {
|
|
||||||
id: null,
|
|
||||||
key: '',
|
|
||||||
name: '',
|
|
||||||
icon: '',
|
|
||||||
phone: '',
|
|
||||||
status: 'ok',
|
|
||||||
context: ''
|
|
||||||
},
|
|
||||||
|
|
||||||
// 对话框控制
|
|
||||||
itemDialog: false,
|
|
||||||
deleteDialog: false,
|
|
||||||
qrDialog: false,
|
|
||||||
saving: false,
|
|
||||||
deleting: false,
|
|
||||||
formValid: false,
|
|
||||||
|
|
||||||
// 选中的物品和删除项
|
|
||||||
selectedItem: null,
|
|
||||||
deleteItem: null,
|
|
||||||
|
|
||||||
// 表格配置
|
|
||||||
headers: [
|
|
||||||
{ title: 'ID', key: 'id', sortable: true },
|
|
||||||
{ title: '物品名称', key: 'name', sortable: true },
|
|
||||||
{ title: '标识码', key: 'key', sortable: true },
|
|
||||||
{ title: '状态', key: 'status', sortable: true },
|
|
||||||
{ title: '创建时间', key: 'created_at', sortable: true },
|
|
||||||
{ title: '操作', key: 'actions', sortable: false }
|
|
||||||
],
|
|
||||||
|
|
||||||
statusOptions: [
|
|
||||||
{ title: '全部状态', value: 'all' },
|
|
||||||
{ title: '正常', value: 'ok' },
|
|
||||||
{ title: '丢失', value: 'lost' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
/**
|
|
||||||
* 过滤后的物品列表
|
|
||||||
*
|
|
||||||
* 根据搜索文本和状态筛选条件过滤物品列表
|
|
||||||
* @returns {Array} 过滤后的物品数组
|
|
||||||
*/
|
|
||||||
filteredItems() {
|
|
||||||
let result = [...this.items];
|
|
||||||
|
|
||||||
// 应用状态筛选
|
|
||||||
if (this.statusFilter !== 'all') {
|
|
||||||
result = result.filter(item => item.status === this.statusFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 打开物品对话框
|
|
||||||
*
|
|
||||||
* @param {Object|null} item - 要编辑的物品,为null时表示添加新物品
|
|
||||||
*/
|
|
||||||
openItemDialog(item = null) {
|
|
||||||
if (item) {
|
|
||||||
this.editItem = JSON.parse(JSON.stringify(item)); // 深拷贝
|
|
||||||
} else {
|
|
||||||
this.editItem = JSON.parse(JSON.stringify(this.defaultItem));
|
|
||||||
// 为新物品生成一个随机标识码
|
|
||||||
this.editItem.key = this.generateRandomKey();
|
|
||||||
}
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.itemForm) {
|
|
||||||
this.$refs.itemForm.resetValidation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.itemDialog = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存物品
|
|
||||||
*
|
|
||||||
* 根据是否有ID决定是添加新物品还是更新现有物品
|
|
||||||
*/
|
|
||||||
async saveItem() {
|
|
||||||
if (!this.formValid) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.saving = true;
|
|
||||||
let data;
|
|
||||||
|
|
||||||
if (this.editItem.id) {
|
|
||||||
// 更新现有物品
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const { id, key, name, icon, phone, status, context } = this.editItem;
|
|
||||||
|
|
||||||
params.append('id', id);
|
|
||||||
params.append('key', key);
|
|
||||||
params.append('name', name);
|
|
||||||
params.append('icon', icon || '');
|
|
||||||
params.append('phone', phone);
|
|
||||||
params.append('status', status);
|
|
||||||
|
|
||||||
// 只有在状态为lost且有context时,才添加context参数
|
|
||||||
if (status === 'lost' && context) {
|
|
||||||
params.append('context', context);
|
|
||||||
}
|
|
||||||
|
|
||||||
data = await apiService.patch(`/api/admin/items?${params.toString()}`, '');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// 添加新物品
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
const { key, name, icon, phone } = this.editItem;
|
|
||||||
|
|
||||||
params.append('key', key);
|
|
||||||
params.append('name', name);
|
|
||||||
params.append('icon', icon || '');
|
|
||||||
params.append('phone', phone);
|
|
||||||
|
|
||||||
data = await apiService.post(`/api/admin/items?${params.toString()}`, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.code !== 0) {
|
|
||||||
throw new Error(data.msg || '保存物品失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'success',
|
|
||||||
message: this.editItem.id ? '物品更新成功' : '物品添加成功'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.itemDialog = false;
|
|
||||||
this.refreshItems(); // 刷新物品列表
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存物品错误:', error);
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'error',
|
|
||||||
message: error.message || '保存物品失败'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.saving = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认删除物品
|
|
||||||
*
|
|
||||||
* @param {Object} item - 要删除的物品
|
|
||||||
*/
|
|
||||||
confirmDelete(item) {
|
|
||||||
this.deleteItem = item;
|
|
||||||
this.deleteDialog = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认删除物品
|
|
||||||
*/
|
|
||||||
async deleteItemConfirm() {
|
|
||||||
if (!this.deleteItem || !this.deleteItem.id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.deleting = true;
|
|
||||||
|
|
||||||
const data = await apiService.delete(`/api/admin/items?id=${encodeURIComponent(this.deleteItem.id)}`);
|
|
||||||
|
|
||||||
if (data.code !== 0) {
|
|
||||||
throw new Error(data.msg || '删除物品失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'success',
|
|
||||||
message: '物品已成功删除'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.deleteDialog = false;
|
|
||||||
this.refreshItems(); // 刷新物品列表
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除物品错误:', error);
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'error',
|
|
||||||
message: error.message || '删除物品失败'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
this.deleting = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示二维码
|
|
||||||
*
|
|
||||||
* @param {Object} item - 要显示二维码的物品
|
|
||||||
*/
|
|
||||||
showQRCode(item) {
|
|
||||||
this.selectedItem = item;
|
|
||||||
this.qrDialog = true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取二维码URL
|
|
||||||
*
|
|
||||||
* @param {string} key - 物品标识码
|
|
||||||
* @returns {string} 二维码图片URL
|
|
||||||
*/
|
|
||||||
getQRCodeUrl(key) {
|
|
||||||
// 使用QR Server API生成二维码
|
|
||||||
const currentUrl = window.location.origin;
|
|
||||||
const foundUrl = `${currentUrl}/found?key=${encodeURIComponent(key)}`;
|
|
||||||
return `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(foundUrl)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成随机标识码
|
|
||||||
*
|
|
||||||
* @returns {string} 随机生成的标识码
|
|
||||||
*/
|
|
||||||
generateRandomKey() {
|
|
||||||
// 生成一个8位的随机字母数字组合
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态对应的颜色
|
|
||||||
*
|
|
||||||
* @param {string} status - 物品状态
|
|
||||||
* @returns {string} 对应的颜色名称
|
|
||||||
*/
|
|
||||||
getStatusColor(status) {
|
|
||||||
const statusMap = {
|
|
||||||
ok: "success",
|
|
||||||
lost: "error",
|
|
||||||
default: "grey"
|
|
||||||
};
|
|
||||||
return statusMap[status] || statusMap.default;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态对应的文本
|
|
||||||
*
|
|
||||||
* @param {string} status - 物品状态
|
|
||||||
* @returns {string} 对应的状态文本
|
|
||||||
*/
|
|
||||||
getStatusText(status) {
|
|
||||||
const statusMap = {
|
|
||||||
ok: "正常",
|
|
||||||
lost: "丢失",
|
|
||||||
default: "未知"
|
|
||||||
};
|
|
||||||
return statusMap[status] || statusMap.default;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化日期显示
|
|
||||||
*
|
|
||||||
* @param {string} create_time - 日期字符串
|
|
||||||
* @returns {string} 格式化的日期文本
|
|
||||||
*/
|
|
||||||
formatDate(dateStr) {
|
|
||||||
if (!dateStr) return "未知时间";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}).format(date);
|
|
||||||
} catch (e) {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置所有筛选条件
|
|
||||||
*/
|
|
||||||
resetFilters() {
|
|
||||||
this.search = '';
|
|
||||||
this.statusFilter = 'all';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新物品列表
|
|
||||||
*/
|
|
||||||
refreshItems() {
|
|
||||||
this.$emit('refresh');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 确保数据表格在移动设备上响应式滚动 */
|
/* 确保数据表格在移动设备上响应式滚动 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
import CacheStatus from '@/components/CacheStatus.vue'
|
||||||
|
|
||||||
|
const userInfo = {
|
||||||
|
username: 'admin',
|
||||||
|
email: 'admin@example.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
darkMode: false,
|
||||||
|
notifications: true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-h4 mb-4">用户设置</h2>
|
<h2 class="text-h4 mb-4">用户设置</h2>
|
||||||
@@ -67,27 +82,4 @@
|
|||||||
|
|
||||||
<p class="text-caption text-center mt-4">更多设置功能正在开发中...</p>
|
<p class="text-caption text-center mt-4">更多设置功能正在开发中...</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import CacheStatus from '@/components/CacheStatus.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'UserSettingsComponent',
|
|
||||||
components: {
|
|
||||||
CacheStatus
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
userInfo: {
|
|
||||||
username: 'admin',
|
|
||||||
email: 'admin@example.com'
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
darkMode: false,
|
|
||||||
notifications: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,4 +1,68 @@
|
|||||||
<!-- src/views/Admin.vue -->
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import apiService from '@/services/api_service'
|
||||||
|
import Dashboard from '@/components/admin/Dashboard.vue'
|
||||||
|
import ItemsManagement from '@/components/admin/ItemsManagement.vue'
|
||||||
|
import UserSettings from '@/components/admin/UserSettings.vue'
|
||||||
|
import AboutSystem from '@/components/admin/AboutSystem.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 界面控制
|
||||||
|
const drawer = ref(false)
|
||||||
|
const currentTab = ref('dashboard')
|
||||||
|
const items = ref([])
|
||||||
|
|
||||||
|
// 检查用户是否已登录
|
||||||
|
const checkAuth = () => {
|
||||||
|
const token = localStorage.getItem('user-token')
|
||||||
|
if (!token) {
|
||||||
|
router.push({
|
||||||
|
path: '/login',
|
||||||
|
query: { redirect: router.currentRoute.value.fullPath }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取物品列表
|
||||||
|
const fetchItems = async () => {
|
||||||
|
try {
|
||||||
|
const data = await apiService.get('/api/admin/items')
|
||||||
|
|
||||||
|
if (data.code === 0 && Array.isArray(data.data)) {
|
||||||
|
items.value = data.data
|
||||||
|
} else {
|
||||||
|
throw new Error(data.msg || '获取物品列表失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取物品列表错误:', error)
|
||||||
|
nextTick(() => {
|
||||||
|
emit('show-toast', {
|
||||||
|
color: 'error',
|
||||||
|
message: error.message || '加载物品数据失败'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem('user-token')
|
||||||
|
router.push('/login')
|
||||||
|
nextTick(() => {
|
||||||
|
emit('show-toast', {
|
||||||
|
color: 'info',
|
||||||
|
message: '您已成功退出登录'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件创建时执行
|
||||||
|
checkAuth()
|
||||||
|
fetchItems()
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="admin-container">
|
<v-container fluid class="admin-container">
|
||||||
<!-- 页面顶部应用栏 -->
|
<!-- 页面顶部应用栏 -->
|
||||||
@@ -61,103 +125,6 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 管理面板组件
|
|
||||||
*
|
|
||||||
* 提供物品管理功能,包括添加、编辑、删除物品,
|
|
||||||
* 以及查看物品状态和生成二维码等功能。
|
|
||||||
*
|
|
||||||
* 此组件还包含仪表盘视图,显示物品统计信息和最近活动。
|
|
||||||
*/
|
|
||||||
import apiService from '@/services/api_service';
|
|
||||||
import Dashboard from '@/components/admin/Dashboard.vue';
|
|
||||||
import ItemsManagement from '@/components/admin/ItemsManagement.vue';
|
|
||||||
import UserSettings from '@/components/admin/UserSettings.vue';
|
|
||||||
import AboutSystem from '@/components/admin/AboutSystem.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'AdminView',
|
|
||||||
components: {
|
|
||||||
Dashboard,
|
|
||||||
ItemsManagement,
|
|
||||||
UserSettings,
|
|
||||||
AboutSystem
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
// 界面控制
|
|
||||||
drawer: false,
|
|
||||||
currentTab: 'dashboard',
|
|
||||||
items: [], // 保存物品数据以便共享给子组件
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
// 检查用户是否已登录
|
|
||||||
this.checkAuth();
|
|
||||||
// 获取物品列表
|
|
||||||
this.fetchItems();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 检查用户是否已登录
|
|
||||||
*
|
|
||||||
* 如果未登录,重定向到登录页面
|
|
||||||
*/
|
|
||||||
checkAuth() {
|
|
||||||
const token = localStorage.getItem('user-token');
|
|
||||||
if (!token) {
|
|
||||||
this.$router.push({
|
|
||||||
path: '/login',
|
|
||||||
query: { redirect: this.$route.fullPath }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取物品列表
|
|
||||||
*
|
|
||||||
* 从API获取所有物品数据
|
|
||||||
*/
|
|
||||||
async fetchItems() {
|
|
||||||
try {
|
|
||||||
const data = await apiService.get('/api/admin/items');
|
|
||||||
|
|
||||||
if (data.code === 0 && Array.isArray(data.data)) {
|
|
||||||
this.items = data.data;
|
|
||||||
} else {
|
|
||||||
throw new Error(data.msg || '获取物品列表失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物品列表错误:', error);
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'error',
|
|
||||||
message: error.message || '加载物品数据失败'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 退出登录
|
|
||||||
*/
|
|
||||||
logout() {
|
|
||||||
localStorage.removeItem('user-token');
|
|
||||||
this.$router.push('/login');
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'info',
|
|
||||||
message: '您已成功退出登录'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.admin-container {
|
.admin-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@@ -1,4 +1,142 @@
|
|||||||
<!-- src/views/Found.vue -->
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import apiService from '@/services/api_service'
|
||||||
|
import storageService from '@/services/storage_service'
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const key = ref(null)
|
||||||
|
const item = ref(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref(null)
|
||||||
|
const showQRDialog = ref(false)
|
||||||
|
const isFromCache = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的颜色
|
||||||
|
* @param {string} status - 物品状态
|
||||||
|
* @returns {string} 对应的颜色名称
|
||||||
|
*/
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
ok: "success",
|
||||||
|
lost: "error",
|
||||||
|
default: "grey"
|
||||||
|
}
|
||||||
|
return statusMap[status] || statusMap.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态对应的文本
|
||||||
|
* @param {string} status - 物品状态
|
||||||
|
* @returns {string} 对应的状态文本
|
||||||
|
*/
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
ok: "正常",
|
||||||
|
lost: "丢失",
|
||||||
|
default: "未知"
|
||||||
|
}
|
||||||
|
return statusMap[status] || statusMap.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期显示
|
||||||
|
* @param {string} dateStr - 日期字符串
|
||||||
|
* @returns {string} 格式化的日期文本
|
||||||
|
*/
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return "未知时间"
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(date)
|
||||||
|
} catch (e) {
|
||||||
|
return dateStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从缓存加载数据并获取最新数据
|
||||||
|
* 优先显示本地缓存的数据,同时从API获取最新数据
|
||||||
|
*/
|
||||||
|
const loadFromCacheAndFetch = async () => {
|
||||||
|
try {
|
||||||
|
// 先尝试从缓存获取数据
|
||||||
|
const cachedItem = storageService.getItemFromCache(key.value)
|
||||||
|
|
||||||
|
if (cachedItem) {
|
||||||
|
// 如果有缓存,立即显示
|
||||||
|
item.value = cachedItem
|
||||||
|
isFromCache.value = true
|
||||||
|
loading.value = true // 保持加载状态,同时获取最新数据
|
||||||
|
|
||||||
|
// 在后台获取最新数据
|
||||||
|
fetchItemDetails(true)
|
||||||
|
} else {
|
||||||
|
// 没有缓存,直接获取最新数据
|
||||||
|
loading.value = true
|
||||||
|
fetchItemDetails(false)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading cached data:", err)
|
||||||
|
// 如果缓存加载失败,直接获取最新数据
|
||||||
|
fetchItemDetails(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取物品详情数据
|
||||||
|
* @param {boolean} isBackground - 是否在后台获取数据(已显示缓存数据)
|
||||||
|
*/
|
||||||
|
const fetchItemDetails = async (isBackground = false) => {
|
||||||
|
try {
|
||||||
|
if (!isBackground) {
|
||||||
|
loading.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiService.getObject(key.value)
|
||||||
|
|
||||||
|
// 更新本地缓存
|
||||||
|
storageService.saveItemToCache(key.value, data)
|
||||||
|
|
||||||
|
// 更新界面数据
|
||||||
|
item.value = data
|
||||||
|
isFromCache.value = false
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching item details:", err)
|
||||||
|
|
||||||
|
// 如果是后台请求且已有缓存数据显示,则不显示错误
|
||||||
|
if (!isBackground || !item.value) {
|
||||||
|
error.value = "获取物品信息失败:" + err.message
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时执行
|
||||||
|
onMounted(() => {
|
||||||
|
// 从URL获取物品的key
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
key.value = urlParams.get('key')
|
||||||
|
|
||||||
|
if (key.value) {
|
||||||
|
// 尝试先从缓存获取数据
|
||||||
|
loadFromCacheAndFetch()
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
error.value = "缺少物品标识,无法获取信息"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container class="found-container">
|
<v-container class="found-container">
|
||||||
<v-row justify="center">
|
<v-row justify="center">
|
||||||
@@ -115,160 +253,6 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 物品详情页面
|
|
||||||
*
|
|
||||||
* 显示找到的物品的详细信息,包括联系方式、物品状态等
|
|
||||||
* 支持从本地缓存加载数据,提高页面加载速度
|
|
||||||
*/
|
|
||||||
import apiService from '@/services/api_service';
|
|
||||||
import storageService from '@/services/storage_service';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "FoundView",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
key: null,
|
|
||||||
item: null,
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
showQRDialog: false,
|
|
||||||
isFromCache: false, // 标识数据是否来自缓存
|
|
||||||
};
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
// 从URL获取物品的key
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
this.key = urlParams.get('key');
|
|
||||||
|
|
||||||
if (this.key) {
|
|
||||||
// 尝试先从缓存获取数据
|
|
||||||
this.loadFromCacheAndFetch();
|
|
||||||
} else {
|
|
||||||
this.loading = false;
|
|
||||||
this.error = "缺少物品标识,无法获取信息";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 从缓存加载数据并获取最新数据
|
|
||||||
*
|
|
||||||
* 优先显示本地缓存的数据,同时从API获取最新数据
|
|
||||||
*/
|
|
||||||
async loadFromCacheAndFetch() {
|
|
||||||
try {
|
|
||||||
// 先尝试从缓存获取数据
|
|
||||||
const cachedItem = storageService.getItemFromCache(this.key);
|
|
||||||
|
|
||||||
if (cachedItem) {
|
|
||||||
// 如果有缓存,立即显示
|
|
||||||
this.item = cachedItem;
|
|
||||||
this.isFromCache = true;
|
|
||||||
this.loading = true; // 保持加载状态,同时获取最新数据
|
|
||||||
|
|
||||||
// 在后台获取最新数据
|
|
||||||
this.fetchItemDetails(true);
|
|
||||||
} else {
|
|
||||||
// 没有缓存,直接获取最新数据
|
|
||||||
this.loading = true;
|
|
||||||
this.fetchItemDetails(false);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error loading cached data:", err);
|
|
||||||
// 如果缓存加载失败,直接获取最新数据
|
|
||||||
this.fetchItemDetails(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取物品详情数据
|
|
||||||
*
|
|
||||||
* @param {boolean} isBackground - 是否在后台获取数据(已显示缓存数据)
|
|
||||||
*/
|
|
||||||
async fetchItemDetails(isBackground = false) {
|
|
||||||
try {
|
|
||||||
if (!isBackground) {
|
|
||||||
this.loading = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await apiService.getObject(this.key);
|
|
||||||
|
|
||||||
// 更新本地缓存
|
|
||||||
storageService.saveItemToCache(this.key, data);
|
|
||||||
|
|
||||||
// 更新界面数据
|
|
||||||
this.item = data;
|
|
||||||
this.isFromCache = false;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error fetching item details:", err);
|
|
||||||
|
|
||||||
// 如果是后台请求且已有缓存数据显示,则不显示错误
|
|
||||||
if (!isBackground || !this.item) {
|
|
||||||
this.error = "获取物品信息失败:" + err.message;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态对应的颜色
|
|
||||||
*
|
|
||||||
* @param {string} status - 物品状态
|
|
||||||
* @returns {string} 对应的颜色名称
|
|
||||||
*/
|
|
||||||
getStatusColor(status) {
|
|
||||||
const statusMap = {
|
|
||||||
ok: "success",
|
|
||||||
lost: "error",
|
|
||||||
default: "grey"
|
|
||||||
};
|
|
||||||
return statusMap[status] || statusMap.default;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态对应的文本
|
|
||||||
*
|
|
||||||
* @param {string} status - 物品状态
|
|
||||||
* @returns {string} 对应的状态文本
|
|
||||||
*/
|
|
||||||
getStatusText(status) {
|
|
||||||
const statusMap = {
|
|
||||||
ok: "正常",
|
|
||||||
lost: "丢失",
|
|
||||||
default: "未知"
|
|
||||||
};
|
|
||||||
return statusMap[status] || statusMap.default;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 格式化日期显示
|
|
||||||
*
|
|
||||||
* @param {string} dateStr - 日期字符串
|
|
||||||
* @returns {string} 格式化的日期文本
|
|
||||||
*/
|
|
||||||
formatDate(dateStr) {
|
|
||||||
if (!dateStr) return "未知时间";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}).format(date);
|
|
||||||
} catch (e) {
|
|
||||||
return dateStr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.found-container {
|
.found-container {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
@@ -1,3 +1,243 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import storageService from '@/services/storage_service'
|
||||||
|
|
||||||
|
// 缓存相关状态
|
||||||
|
const isFromCache = ref(false)
|
||||||
|
const showCacheAlert = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const cacheTimestamp = ref(null)
|
||||||
|
|
||||||
|
// 本地存储键名
|
||||||
|
const HOME_CACHE_KEY = 'home-data'
|
||||||
|
|
||||||
|
// 格式化缓存时间
|
||||||
|
const formatCacheTime = computed(() => {
|
||||||
|
if (!cacheTimestamp.value) return ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(cacheTimestamp.value)
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}).format(date)
|
||||||
|
} catch (e) {
|
||||||
|
return '未知时间'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从服务加载主页数据
|
||||||
|
const fetchHomeData = async () => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 800))
|
||||||
|
return {
|
||||||
|
socialLinks,
|
||||||
|
devSkills,
|
||||||
|
designSkills,
|
||||||
|
musicSkills,
|
||||||
|
projects,
|
||||||
|
musicWorks,
|
||||||
|
timeline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储加载数据
|
||||||
|
const loadFromCache = () => {
|
||||||
|
try {
|
||||||
|
const cachedItem = storageService.getItemFromCache(HOME_CACHE_KEY)
|
||||||
|
|
||||||
|
if (cachedItem) {
|
||||||
|
const cachedTimestamp = storageService.getCacheTimestamp(HOME_CACHE_KEY)
|
||||||
|
if (cachedTimestamp) {
|
||||||
|
cacheTimestamp.value = cachedTimestamp
|
||||||
|
isFromCache.value = true
|
||||||
|
showCacheAlert.value = true
|
||||||
|
}
|
||||||
|
console.log('Using cached home page data')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading cached data:', error)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存数据到本地存储
|
||||||
|
const saveToCache = async () => {
|
||||||
|
try {
|
||||||
|
const currentData = await fetchHomeData()
|
||||||
|
storageService.saveItemToCache(HOME_CACHE_KEY, currentData)
|
||||||
|
console.log('Home page data cached')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving data to cache:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const refreshData = async () => {
|
||||||
|
refreshing.value = true
|
||||||
|
try {
|
||||||
|
const newData = await fetchHomeData()
|
||||||
|
storageService.saveItemToCache(HOME_CACHE_KEY, newData)
|
||||||
|
isFromCache.value = false
|
||||||
|
showCacheAlert.value = false
|
||||||
|
console.log('Home data refreshed successfully')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing home data:', error)
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时执行
|
||||||
|
onMounted(async () => {
|
||||||
|
const hasCachedData = loadFromCache()
|
||||||
|
|
||||||
|
if (!hasCachedData) {
|
||||||
|
try {
|
||||||
|
await fetchHomeData()
|
||||||
|
saveToCache()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching initial home data:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 静态数据定义
|
||||||
|
|
||||||
|
// 社交媒体链接
|
||||||
|
const socialLinks = [
|
||||||
|
{ icon: 'mdi-github', url: 'https://github.com/Yuerchu' },
|
||||||
|
{ icon: 'mdi-music', url: 'https://music.163.com/#/artist?id=48986728' },
|
||||||
|
{ icon: 'mdi-web', url: 'https://www.yxqi.cn' },
|
||||||
|
{ icon: 'mdi-email', url: 'mailto:admin@yuxiaoqiu.cn' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 开发技能
|
||||||
|
const devSkills = [
|
||||||
|
{ name: 'Python', color: 'amber-darken-1' },
|
||||||
|
{ name: 'Kotlin', color: 'purple-lighten-2' },
|
||||||
|
{ name: 'Golang', color: 'light-blue' },
|
||||||
|
{ name: 'Lua', color: 'blue-darken-4' },
|
||||||
|
{ name: 'C', color: 'red' },
|
||||||
|
{ name: 'HTML/CSS', color: 'red-darken-3' },
|
||||||
|
{ name: 'JavaScript', color: 'lime-darken-3' },
|
||||||
|
{ name: 'Git', color: 'amber-darken-3' },
|
||||||
|
{ name: 'Docker', color: 'light-blue-darken-1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 设计技能
|
||||||
|
const designSkills = [
|
||||||
|
{ name: 'Photoshop', color: 'blue-darken-4' },
|
||||||
|
{ name: 'Premiere', color: 'indigo-darken-3' },
|
||||||
|
{ name: 'After Effects', color: 'indigo-darken-4' },
|
||||||
|
{ name: 'Audition', color: 'purple-darken-4' },
|
||||||
|
{ name: 'Illustrator', color: 'amber-darken-3' },
|
||||||
|
{ name: 'UI/UX', color: 'pink-darken-2' },
|
||||||
|
{ name: 'SAI2', color: 'grey-darken-3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 音乐技能
|
||||||
|
const musicSkills = [
|
||||||
|
{ name: 'FL Studio', color: 'orange-darken-2' },
|
||||||
|
{ name: '作曲', color: 'deep-purple' },
|
||||||
|
{ name: '编曲', color: 'indigo' },
|
||||||
|
{ name: '混音', color: 'blue' },
|
||||||
|
{ name: '母带处理', color: 'teal' },
|
||||||
|
{ name: 'Midi创作', color: 'cyan' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 项目作品
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
title: 'DiskNext',
|
||||||
|
tag: 'B端系统',
|
||||||
|
tagColor: 'primary',
|
||||||
|
image: 'https://cdn.vuetifyjs.com/images/cards/sunshine.jpg',
|
||||||
|
description: '基于 NiceGUI 打造的高性能网盘系统,提供快速、安全的文件存储与分享服务。',
|
||||||
|
link: 'https://pan.yxqi.cn',
|
||||||
|
tech: ['Python', 'NiceGUI', 'SQLite', 'Docker']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Findreve',
|
||||||
|
tag: 'C端应用',
|
||||||
|
tagColor: 'success',
|
||||||
|
image: 'https://cdn.vuetifyjs.com/images/cards/road.jpg',
|
||||||
|
description: '个人主页与物品丢失找回系统,帮助用户追踪和找回丢失物品。',
|
||||||
|
link: 'https://i.yxqi.cn',
|
||||||
|
tech: ['Vue', 'Vuetify', 'FastAPI', 'MySQL']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HeyAuth',
|
||||||
|
tag: 'B+C端系统',
|
||||||
|
tagColor: 'info',
|
||||||
|
image: 'https://cdn.vuetifyjs.com/images/cards/plane.jpg',
|
||||||
|
description: '多应用授权系统,提供统一的身份验证和权限管理服务。',
|
||||||
|
link: 'https://auth.yxqi.cn',
|
||||||
|
tech: ['Python', 'JWT', 'OAuth2', 'Redis']
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 音乐作品
|
||||||
|
const musicWorks = [
|
||||||
|
{
|
||||||
|
title: '与枫同奔 Run With Fun',
|
||||||
|
tag: '词曲',
|
||||||
|
description: '我愿如流星赶月那样飞奔,向着远方的梦想不断前行。',
|
||||||
|
link: 'https://music.163.com/#/song?id=2148944359',
|
||||||
|
cover: 'https://cdn.vuetifyjs.com/images/cards/foster.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'HeyFun\'s Story',
|
||||||
|
tag: '自设印象曲',
|
||||||
|
description: '飞奔在星辰大海之间的少年,勇敢探索未知的世界。',
|
||||||
|
link: 'https://music.163.com/#/song?id=1889436124',
|
||||||
|
cover: 'https://cdn.vuetifyjs.com/images/cards/house.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '2020Fall',
|
||||||
|
tag: '年度纯音乐',
|
||||||
|
description: '耗时6个月完成的年度纯音乐作品,记录2020年的回忆。',
|
||||||
|
link: 'https://music.163.com/#/song?id=1863630345',
|
||||||
|
cover: 'https://cdn.vuetifyjs.com/images/cards/store.jpg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 时间线
|
||||||
|
const timeline = [
|
||||||
|
{
|
||||||
|
title: '梦开始的地方',
|
||||||
|
date: '2022年1月21日',
|
||||||
|
content: '购买了第一台服务器,并搭建了第一个 Wordpress 站点,开始了我的网络创作之旅。',
|
||||||
|
color: 'primary',
|
||||||
|
icon: 'mdi-server'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '音乐作品发布',
|
||||||
|
date: '2023年10月29日',
|
||||||
|
content: '在网易云音乐发布了收官作《与枫同奔 Release》,截止到 2025 年 4 月 21 日获得了 7000+ 播放。',
|
||||||
|
color: 'deep-purple',
|
||||||
|
icon: 'mdi-music'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '自建生态计划开始',
|
||||||
|
date: '2024年3月1日',
|
||||||
|
content: '从 Cloudreve 项目脱离,开始自建网盘系统 DiskNext ,迈出了建立个人技术生态的第一步。',
|
||||||
|
color: 'amber',
|
||||||
|
icon: 'mdi-cloud'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '当前进展',
|
||||||
|
date: '现在',
|
||||||
|
content: '目前正在开发 HeyAuth、Findreve、DiskNext 三个核心系统,构建完整的个人应用生态。',
|
||||||
|
color: 'success',
|
||||||
|
icon: 'mdi-rocket'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<!-- 顶部封面区 -->
|
<!-- 顶部封面区 -->
|
||||||
@@ -279,303 +519,6 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
/**
|
|
||||||
* 首页视图组件
|
|
||||||
*
|
|
||||||
* 这是一个现代化的个人主页视图,包含个人介绍、技能展示、项目展示、
|
|
||||||
* 音乐作品、时间线和联系方式等部分,采用响应式设计适配各种设备。
|
|
||||||
* 支持本地数据缓存,提升用户体验。
|
|
||||||
*/
|
|
||||||
import { ref, onMounted, computed } from 'vue';
|
|
||||||
import storageService from '@/services/storage_service';
|
|
||||||
|
|
||||||
// 缓存相关状态
|
|
||||||
const isFromCache = ref(false);
|
|
||||||
const showCacheAlert = ref(false);
|
|
||||||
const refreshing = ref(false);
|
|
||||||
const cacheTimestamp = ref<number | null>(null);
|
|
||||||
|
|
||||||
// 本地存储键名
|
|
||||||
const HOME_CACHE_KEY = 'home-data';
|
|
||||||
|
|
||||||
// 格式化缓存时间
|
|
||||||
const formatCacheTime = computed(() => {
|
|
||||||
if (!cacheTimestamp.value) return '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const date = new Date(cacheTimestamp.value);
|
|
||||||
return new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}).format(date);
|
|
||||||
} catch (e) {
|
|
||||||
return '未知时间';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从服务加载主页数据
|
|
||||||
*
|
|
||||||
* 在实际应用中,这里应该是API请求
|
|
||||||
* 目前仅为模拟API请求的示例函数
|
|
||||||
* @returns {Promise<Object>} 首页数据对象
|
|
||||||
*/
|
|
||||||
const fetchHomeData = async (): Promise<any> => {
|
|
||||||
// 模拟API延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 800));
|
|
||||||
|
|
||||||
// 在真实应用中,这里应该是API调用
|
|
||||||
// const response = await fetch('/api/home');
|
|
||||||
// return response.json();
|
|
||||||
|
|
||||||
// 这里使用静态数据模拟API返回
|
|
||||||
return {
|
|
||||||
socialLinks,
|
|
||||||
devSkills,
|
|
||||||
designSkills,
|
|
||||||
musicSkills,
|
|
||||||
projects,
|
|
||||||
musicWorks,
|
|
||||||
timeline
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从本地存储加载数据
|
|
||||||
*
|
|
||||||
* 使用统一的存储服务获取缓存的首页数据
|
|
||||||
* 如果数据存在且未过期,则使用缓存数据
|
|
||||||
*/
|
|
||||||
const loadFromCache = () => {
|
|
||||||
try {
|
|
||||||
// 从统一存储服务获取缓存数据
|
|
||||||
const cachedItem = storageService.getItemFromCache(HOME_CACHE_KEY);
|
|
||||||
|
|
||||||
if (cachedItem) {
|
|
||||||
// 找到有效缓存
|
|
||||||
|
|
||||||
// 在实际应用中,可以使用缓存数据更新组件状态
|
|
||||||
// updateComponentState(cachedItem);
|
|
||||||
|
|
||||||
// 获取缓存时间戳以显示在UI上
|
|
||||||
const cachedTimestamp = storageService.getCacheTimestamp(HOME_CACHE_KEY);
|
|
||||||
if (cachedTimestamp) {
|
|
||||||
cacheTimestamp.value = cachedTimestamp;
|
|
||||||
isFromCache.value = true;
|
|
||||||
showCacheAlert.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Using cached home page data');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading cached data:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存数据到本地存储
|
|
||||||
*
|
|
||||||
* 将首页当前状态保存到统一存储服务中
|
|
||||||
*/
|
|
||||||
const saveToCache = async () => {
|
|
||||||
try {
|
|
||||||
// 在实际应用中,这里应该获取当前组件的最新状态
|
|
||||||
// 或者直接缓存API返回的数据
|
|
||||||
const currentData = await fetchHomeData();
|
|
||||||
|
|
||||||
// 使用存储服务保存数据
|
|
||||||
storageService.saveItemToCache(HOME_CACHE_KEY, currentData);
|
|
||||||
console.log('Home page data cached');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving data to cache:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 刷新数据
|
|
||||||
*
|
|
||||||
* 从服务器获取最新数据并更新缓存
|
|
||||||
*/
|
|
||||||
const refreshData = async () => {
|
|
||||||
refreshing.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 从服务器获取最新数据
|
|
||||||
const newData = await fetchHomeData();
|
|
||||||
|
|
||||||
// 在实际应用中,需要用新数据更新组件状态
|
|
||||||
// updateComponentWithNewData(newData);
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
storageService.saveItemToCache(HOME_CACHE_KEY, newData);
|
|
||||||
|
|
||||||
// 更新UI状态
|
|
||||||
isFromCache.value = false;
|
|
||||||
showCacheAlert.value = false;
|
|
||||||
|
|
||||||
console.log('Home data refreshed successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing home data:', error);
|
|
||||||
} finally {
|
|
||||||
refreshing.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 在组件挂载时尝试加载缓存数据,无论是否有缓存都保存当前状态
|
|
||||||
onMounted(async () => {
|
|
||||||
// 尝试加载缓存数据
|
|
||||||
const hasCachedData = loadFromCache();
|
|
||||||
|
|
||||||
// 如果没有缓存数据或缓存已过期,获取新数据并保存
|
|
||||||
if (!hasCachedData) {
|
|
||||||
try {
|
|
||||||
// 模拟API请求
|
|
||||||
await fetchHomeData();
|
|
||||||
// 保存到缓存
|
|
||||||
saveToCache();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching initial home data:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 社交媒体链接
|
|
||||||
const socialLinks = [
|
|
||||||
{ icon: 'mdi-github', url: 'https://github.com/Yuerchu' },
|
|
||||||
{ icon: 'mdi-music', url: 'https://music.163.com/#/artist?id=48986728' },
|
|
||||||
{ icon: 'mdi-web', url: 'https://www.yxqi.cn' },
|
|
||||||
{ icon: 'mdi-email', url: 'mailto:admin@yuxiaoqiu.cn' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 开发技能
|
|
||||||
const devSkills = [
|
|
||||||
{ name: 'Python', color: 'amber-darken-1' },
|
|
||||||
{ name: 'Kotlin', color: 'purple-lighten-2' },
|
|
||||||
{ name: 'Golang', color: 'light-blue' },
|
|
||||||
{ name: 'Lua', color: 'blue-darken-4' },
|
|
||||||
{ name: 'C', color: 'red' },
|
|
||||||
{ name: 'HTML/CSS', color: 'red-darken-3' },
|
|
||||||
{ name: 'JavaScript', color: 'lime-darken-3' },
|
|
||||||
{ name: 'Git', color: 'amber-darken-3' },
|
|
||||||
{ name: 'Docker', color: 'light-blue-darken-1' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 设计技能
|
|
||||||
const designSkills = [
|
|
||||||
{ name: 'Photoshop', color: 'blue-darken-4' },
|
|
||||||
{ name: 'Premiere', color: 'indigo-darken-3' },
|
|
||||||
{ name: 'After Effects', color: 'indigo-darken-4' },
|
|
||||||
{ name: 'Audition', color: 'purple-darken-4' },
|
|
||||||
{ name: 'Illustrator', color: 'amber-darken-3' },
|
|
||||||
{ name: 'UI/UX', color: 'pink-darken-2' },
|
|
||||||
{ name: 'SAI2', color: 'grey-darken-3' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 音乐技能
|
|
||||||
const musicSkills = [
|
|
||||||
{ name: 'FL Studio', color: 'orange-darken-2' },
|
|
||||||
{ name: '作曲', color: 'deep-purple' },
|
|
||||||
{ name: '编曲', color: 'indigo' },
|
|
||||||
{ name: '混音', color: 'blue' },
|
|
||||||
{ name: '母带处理', color: 'teal' },
|
|
||||||
{ name: 'Midi创作', color: 'cyan' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 项目作品
|
|
||||||
const projects = [
|
|
||||||
{
|
|
||||||
title: 'DiskNext',
|
|
||||||
tag: 'B端系统',
|
|
||||||
tagColor: 'primary',
|
|
||||||
image: 'https://cdn.vuetifyjs.com/images/cards/sunshine.jpg',
|
|
||||||
description: '基于 NiceGUI 打造的高性能网盘系统,提供快速、安全的文件存储与分享服务。',
|
|
||||||
link: 'https://pan.yxqi.cn',
|
|
||||||
tech: ['Python', 'NiceGUI', 'SQLite', 'Docker']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Findreve',
|
|
||||||
tag: 'C端应用',
|
|
||||||
tagColor: 'success',
|
|
||||||
image: 'https://cdn.vuetifyjs.com/images/cards/road.jpg',
|
|
||||||
description: '个人主页与物品丢失找回系统,帮助用户追踪和找回丢失物品。',
|
|
||||||
link: 'https://i.yxqi.cn',
|
|
||||||
tech: ['Vue', 'Vuetify', 'FastAPI', 'MySQL']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'HeyAuth',
|
|
||||||
tag: 'B+C端系统',
|
|
||||||
tagColor: 'info',
|
|
||||||
image: 'https://cdn.vuetifyjs.com/images/cards/plane.jpg',
|
|
||||||
description: '多应用授权系统,提供统一的身份验证和权限管理服务。',
|
|
||||||
link: 'https://auth.yxqi.cn',
|
|
||||||
tech: ['Python', 'JWT', 'OAuth2', 'Redis']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 音乐作品
|
|
||||||
const musicWorks = [
|
|
||||||
{
|
|
||||||
title: '与枫同奔 Run With Fun',
|
|
||||||
tag: '词曲',
|
|
||||||
description: '我愿如流星赶月那样飞奔,向着远方的梦想不断前行。',
|
|
||||||
link: 'https://music.163.com/#/song?id=2148944359',
|
|
||||||
cover: 'https://cdn.vuetifyjs.com/images/cards/foster.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'HeyFun\'s Story',
|
|
||||||
tag: '自设印象曲',
|
|
||||||
description: '飞奔在星辰大海之间的少年,勇敢探索未知的世界。',
|
|
||||||
link: 'https://music.163.com/#/song?id=1889436124',
|
|
||||||
cover: 'https://cdn.vuetifyjs.com/images/cards/house.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '2020Fall',
|
|
||||||
tag: '年度纯音乐',
|
|
||||||
description: '耗时6个月完成的年度纯音乐作品,记录2020年的回忆。',
|
|
||||||
link: 'https://music.163.com/#/song?id=1863630345',
|
|
||||||
cover: 'https://cdn.vuetifyjs.com/images/cards/store.jpg'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 时间线
|
|
||||||
const timeline = [
|
|
||||||
{
|
|
||||||
title: '梦开始的地方',
|
|
||||||
date: '2022年1月21日',
|
|
||||||
content: '购买了第一台服务器,并搭建了第一个 Wordpress 站点,开始了我的网络创作之旅。',
|
|
||||||
color: 'primary',
|
|
||||||
icon: 'mdi-server'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '音乐作品发布',
|
|
||||||
date: '2023年10月29日',
|
|
||||||
content: '在网易云音乐发布了收官作《与枫同奔 Release》,截止到 2025 年 4 月 21 日获得了 7000+ 播放。',
|
|
||||||
color: 'deep-purple',
|
|
||||||
icon: 'mdi-music'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '自建生态计划开始',
|
|
||||||
date: '2024年3月1日',
|
|
||||||
content: '从 Cloudreve 项目脱离,开始自建网盘系统 DiskNext ,迈出了建立个人技术生态的第一步。',
|
|
||||||
color: 'amber',
|
|
||||||
icon: 'mdi-cloud'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '当前进展',
|
|
||||||
date: '现在',
|
|
||||||
content: '目前正在开发 HeyAuth、Findreve、DiskNext 三个核心系统,构建完整的个人应用生态。',
|
|
||||||
color: 'success',
|
|
||||||
icon: 'mdi-rocket'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.max-width-text {
|
.max-width-text {
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
|||||||
@@ -1,4 +1,102 @@
|
|||||||
<!-- src/views/Login.vue -->
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import apiService from '@/services/api_service'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const formValid = ref(false)
|
||||||
|
const tokenExpired = ref(false)
|
||||||
|
const checkingToken = ref(true)
|
||||||
|
const loginForm = ref(null)
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const usernameRules = [
|
||||||
|
v => !!v || '用户名不能为空'
|
||||||
|
]
|
||||||
|
const passwordRules = [
|
||||||
|
v => !!v || '密码不能为空',
|
||||||
|
v => (v && v.length >= 6) || '密码长度不能小于6位'
|
||||||
|
]
|
||||||
|
|
||||||
|
// 验证现有令牌
|
||||||
|
const validateExistingToken = async () => {
|
||||||
|
try {
|
||||||
|
checkingToken.value = true
|
||||||
|
const token = localStorage.getItem('user-token')
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const isValid = await apiService.validateToken()
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
console.log('令牌有效,正在重定向')
|
||||||
|
const redirectPath = route.query.redirect || '/'
|
||||||
|
router.push(redirectPath)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
console.log('令牌无效,已清除')
|
||||||
|
localStorage.removeItem('user-token')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证令牌时出错:', error)
|
||||||
|
} finally {
|
||||||
|
checkingToken.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理用户登录
|
||||||
|
const login = async () => {
|
||||||
|
const { valid } = await loginForm.value.validate()
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiService.login(username.value, password.value)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 登录成功
|
||||||
|
emit('show-toast', {
|
||||||
|
color: 'success',
|
||||||
|
message: '登录成功,正在跳转...'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录成功后重定向
|
||||||
|
const redirectPath = route.query.redirect || '/'
|
||||||
|
router.push(redirectPath)
|
||||||
|
} else {
|
||||||
|
// 登录失败
|
||||||
|
errorMessage.value = result.error
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error)
|
||||||
|
errorMessage.value = error.message || '登录过程中发生错误,请稍后再试'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查是否是因为令牌过期而重定向
|
||||||
|
tokenExpired.value = route.query.expired === 'true'
|
||||||
|
|
||||||
|
// 如果不是因为令牌过期重定向,则验证令牌
|
||||||
|
if (!tokenExpired.value) {
|
||||||
|
validateExistingToken()
|
||||||
|
} else {
|
||||||
|
checkingToken.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container class="fill-height">
|
<v-container class="fill-height">
|
||||||
<v-row justify="center" align="center">
|
<v-row justify="center" align="center">
|
||||||
@@ -84,126 +182,6 @@
|
|||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 登录页面组件
|
|
||||||
*
|
|
||||||
* 处理用户登录认证,成功后保存token并重定向
|
|
||||||
* 包含表单验证、记住我功能和错误处理
|
|
||||||
* 支持显示令牌过期的提示
|
|
||||||
* 支持自动验证现有令牌并跳转
|
|
||||||
*/
|
|
||||||
import apiService from '@/services/api_service';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
errorMessage: '',
|
|
||||||
loading: false,
|
|
||||||
formValid: false,
|
|
||||||
tokenExpired: false,
|
|
||||||
checkingToken: true, // 是否正在验证令牌
|
|
||||||
usernameRules: [
|
|
||||||
v => !!v || '用户名不能为空'
|
|
||||||
],
|
|
||||||
passwordRules: [
|
|
||||||
v => !!v || '密码不能为空',
|
|
||||||
v => (v && v.length >= 6) || '密码长度不能小于6位'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
// 检查是否是因为令牌过期而重定向
|
|
||||||
this.tokenExpired = this.$route.query.expired === 'true';
|
|
||||||
|
|
||||||
// 如果不是因为令牌过期重定向,则验证令牌
|
|
||||||
if (!this.tokenExpired) {
|
|
||||||
this.validateExistingToken();
|
|
||||||
} else {
|
|
||||||
this.checkingToken = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* 验证现有令牌
|
|
||||||
*
|
|
||||||
* 检查本地是否有JWT令牌,如果有则验证其有效性
|
|
||||||
* 如果令牌有效,自动重定向到目标页面
|
|
||||||
*/
|
|
||||||
async validateExistingToken() {
|
|
||||||
try {
|
|
||||||
this.checkingToken = true;
|
|
||||||
const token = localStorage.getItem('user-token');
|
|
||||||
|
|
||||||
// 如果有令牌,验证其有效性
|
|
||||||
if (token) {
|
|
||||||
const isValid = await apiService.validateToken();
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
// 令牌有效,重定向到目标页面
|
|
||||||
console.log('令牌有效,正在重定向');
|
|
||||||
const redirectPath = this.$route.query.redirect || '/';
|
|
||||||
this.$router.push(redirectPath);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// 令牌无效,清除
|
|
||||||
console.log('令牌无效,已清除');
|
|
||||||
localStorage.removeItem('user-token');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证令牌时出错:', error);
|
|
||||||
} finally {
|
|
||||||
this.checkingToken = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理用户登录
|
|
||||||
*
|
|
||||||
* 发送登录请求到后端API,处理成功和失败情况
|
|
||||||
* 支持表单验证
|
|
||||||
*/
|
|
||||||
async login() {
|
|
||||||
// 表单验证
|
|
||||||
const { valid } = await this.$refs.loginForm.validate();
|
|
||||||
if (!valid) return;
|
|
||||||
|
|
||||||
this.loading = true;
|
|
||||||
this.errorMessage = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await apiService.login(this.username, this.password);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
// 登录成功
|
|
||||||
this.$root.$emit('show-toast', {
|
|
||||||
color: 'success',
|
|
||||||
message: '登录成功,正在跳转...'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 登录成功后重定向
|
|
||||||
const redirectPath = this.$route.query.redirect || '/';
|
|
||||||
this.$router.push(redirectPath);
|
|
||||||
} else {
|
|
||||||
// 登录失败
|
|
||||||
this.errorMessage = result.error;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('登录错误:', error);
|
|
||||||
this.errorMessage = error.message || '登录过程中发生错误,请稍后再试';
|
|
||||||
} finally {
|
|
||||||
this.loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.login-card {
|
.login-card {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 404错误页面组件
|
||||||
|
* 当用户访问不存在的路由时显示此页面
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container class="fill-height">
|
<v-container class="fill-height">
|
||||||
<v-row align="center" justify="center">
|
<v-row align="center" justify="center">
|
||||||
@@ -26,14 +33,4 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* 404错误页面组件
|
|
||||||
* 当用户访问不存在的路由时显示此页面
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
name: 'NotFound'
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
Reference in New Issue
Block a user