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>
|
||||
<v-app>
|
||||
<!-- 添加加载指示器 -->
|
||||
@@ -15,61 +64,6 @@
|
||||
</v-app>
|
||||
</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>
|
||||
.loading-overlay {
|
||||
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>
|
||||
<v-footer
|
||||
app
|
||||
@@ -36,41 +72,6 @@
|
||||
</v-footer>
|
||||
</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">
|
||||
.social-link :deep(.v-icon)
|
||||
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>
|
||||
<v-card class="my-3">
|
||||
<v-card-title class="d-flex align-center">
|
||||
@@ -40,97 +112,4 @@
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</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>
|
||||
</template>
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
const systemInfo = {
|
||||
version: '2.0.0 Alpha',
|
||||
releaseDate: '2025-07-15',
|
||||
framework: 'FastAPI + Vue'
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-h4 mb-4">关于 Findreve</h2>
|
||||
@@ -61,19 +70,4 @@
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'AboutSystemComponent',
|
||||
data() {
|
||||
return {
|
||||
systemInfo: {
|
||||
version: '2.0.0 Alpha',
|
||||
releaseDate: '2025-07-15',
|
||||
framework: 'FastAPI + Vue'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -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>
|
||||
<div>
|
||||
<h2 class="text-h4 mb-4">仪表盘</h2>
|
||||
@@ -40,74 +82,4 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
@@ -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>
|
||||
<div>
|
||||
<div class="d-flex justify-space-between align-center mb-4">
|
||||
@@ -260,345 +511,6 @@
|
||||
</div>
|
||||
</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>
|
||||
/* 确保数据表格在移动设备上响应式滚动 */
|
||||
@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>
|
||||
<div>
|
||||
<h2 class="text-h4 mb-4">用户设置</h2>
|
||||
@@ -67,27 +82,4 @@
|
||||
|
||||
<p class="text-caption text-center mt-4">更多设置功能正在开发中...</p>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
@@ -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>
|
||||
<v-container fluid class="admin-container">
|
||||
<!-- 页面顶部应用栏 -->
|
||||
@@ -61,103 +125,6 @@
|
||||
</v-container>
|
||||
</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>
|
||||
.admin-container {
|
||||
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>
|
||||
<v-container class="found-container">
|
||||
<v-row justify="center">
|
||||
@@ -115,160 +253,6 @@
|
||||
</v-container>
|
||||
</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>
|
||||
.found-container {
|
||||
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>
|
||||
<v-container fluid>
|
||||
<!-- 顶部封面区 -->
|
||||
@@ -279,303 +519,6 @@
|
||||
</v-container>
|
||||
</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>
|
||||
.max-width-text {
|
||||
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>
|
||||
<v-container class="fill-height">
|
||||
<v-row justify="center" align="center">
|
||||
@@ -84,126 +182,6 @@
|
||||
</v-container>
|
||||
</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>
|
||||
.login-card {
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 404错误页面组件
|
||||
* 当用户访问不存在的路由时显示此页面
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container class="fill-height">
|
||||
<v-row align="center" justify="center">
|
||||
@@ -26,14 +33,4 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 404错误页面组件
|
||||
* 当用户访问不存在的路由时显示此页面
|
||||
*/
|
||||
export default {
|
||||
name: 'NotFound'
|
||||
}
|
||||
</script>
|
||||
</template>
|
||||
Reference in New Issue
Block a user