默认头像
This commit is contained in:
parent
5c2469137c
commit
0a8595b308
|
|
@ -80,6 +80,40 @@
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户默认头像" prop="default_avatar">
|
||||||
|
<div class="upload-container">
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleDefaultAvatarSuccess"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
name="file">
|
||||||
|
<img v-if="appearanceConfig.default_avatar" :src="getImageUrl(appearanceConfig.default_avatar)" class="avatar-preview" />
|
||||||
|
<div v-else class="upload-placeholder">
|
||||||
|
<el-icon class="upload-icon"><Plus /></el-icon>
|
||||||
|
<div class="upload-text">点击上传头像</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
<div class="upload-info">
|
||||||
|
<div class="form-tip">建议尺寸: 200x200px</div>
|
||||||
|
<div class="form-tip">支持格式: PNG, JPG</div>
|
||||||
|
<div class="form-tip">文件大小: 最大5MB</div>
|
||||||
|
<div class="form-tip">新用户注册时将使用此头像</div>
|
||||||
|
<el-button
|
||||||
|
v-if="appearanceConfig.default_avatar"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="clearDefaultAvatar">
|
||||||
|
清除头像
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="saveAppearanceConfig" :loading="saving">
|
<el-button type="primary" @click="saveAppearanceConfig" :loading="saving">
|
||||||
<el-icon><Check /></el-icon>
|
<el-icon><Check /></el-icon>
|
||||||
|
|
@ -359,7 +393,8 @@ const originalConfigs = reactive({
|
||||||
|
|
||||||
const appearanceConfig = ref({
|
const appearanceConfig = ref({
|
||||||
app_logo: '',
|
app_logo: '',
|
||||||
about_us_image: ''
|
about_us_image: '',
|
||||||
|
default_avatar: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const generalConfig = ref({
|
const generalConfig = ref({
|
||||||
|
|
@ -553,6 +588,13 @@ const saveAppearanceConfig = async () => {
|
||||||
description: 'About us section image URL'
|
description: 'About us section image URL'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await api.put('/api/v1/admin/config/default_avatar', {
|
||||||
|
value: appearanceConfig.value.default_avatar,
|
||||||
|
type: 'image',
|
||||||
|
category: 'appearance',
|
||||||
|
description: '用户默认头像'
|
||||||
|
})
|
||||||
|
|
||||||
// Update original configs
|
// Update original configs
|
||||||
originalConfigs.appearance = { ...appearanceConfig.value }
|
originalConfigs.appearance = { ...appearanceConfig.value }
|
||||||
|
|
||||||
|
|
@ -803,6 +845,30 @@ const handleAboutSuccess = (response) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle default avatar upload success
|
||||||
|
const handleDefaultAvatarSuccess = (response) => {
|
||||||
|
if (response.code === 0) {
|
||||||
|
appearanceConfig.value.default_avatar = response.data.url
|
||||||
|
ElMessage.success('默认头像上传成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '默认头像上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear default avatar
|
||||||
|
const clearDefaultAvatar = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要清除默认头像吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
appearanceConfig.value.default_avatar = ''
|
||||||
|
} catch {
|
||||||
|
// User cancelled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle QR code upload success
|
// Handle QR code upload success
|
||||||
const handleQrSuccess = (response) => {
|
const handleQrSuccess = (response) => {
|
||||||
if (response.code === 0) {
|
if (response.code === 0) {
|
||||||
|
|
@ -996,6 +1062,42 @@ onMounted(() => {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-uploader {
|
||||||
|
border: 2px dashed #d9d9d9;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background-color: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader:hover {
|
||||||
|
border-color: #409eff;
|
||||||
|
background-color: #f0f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-uploader :deep(.el-upload) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.upload-info {
|
.upload-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
122
backend/run-migration.js
Normal file
122
backend/run-migration.js
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
/**
|
||||||
|
* Safe migration script - checks if columns exist before renaming
|
||||||
|
*/
|
||||||
|
const { sequelize } = require('./src/config/database');
|
||||||
|
|
||||||
|
async function runMigration() {
|
||||||
|
try {
|
||||||
|
console.log('Connecting to database...');
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('Connected successfully!\n');
|
||||||
|
|
||||||
|
// Check and rename hot_services.name_pt to name_es
|
||||||
|
console.log('Checking hot_services table...');
|
||||||
|
const [hotServiceCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `hot_services` LIKE 'name_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hotServiceCols.length > 0) {
|
||||||
|
console.log('Renaming hot_services.name_pt to name_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `hot_services` CHANGE COLUMN `name_pt` `name_es` VARCHAR(100) NOT NULL"
|
||||||
|
);
|
||||||
|
console.log('✓ hot_services.name_pt renamed to name_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ hot_services.name_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and rename category.name_pt to name_es
|
||||||
|
console.log('\nChecking category table...');
|
||||||
|
const [categoryCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `category` LIKE 'name_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (categoryCols.length > 0) {
|
||||||
|
console.log('Renaming category.name_pt to name_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `category` CHANGE COLUMN `name_pt` `name_es` VARCHAR(100) NOT NULL"
|
||||||
|
);
|
||||||
|
console.log('✓ category.name_pt renamed to name_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ category.name_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and rename service.title_pt and description_pt
|
||||||
|
console.log('\nChecking service table...');
|
||||||
|
const [serviceTitleCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `service` LIKE 'title_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serviceTitleCols.length > 0) {
|
||||||
|
console.log('Renaming service.title_pt to title_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `service` CHANGE COLUMN `title_pt` `title_es` VARCHAR(200) NOT NULL"
|
||||||
|
);
|
||||||
|
console.log('✓ service.title_pt renamed to title_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ service.title_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [serviceDescCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `service` LIKE 'description_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (serviceDescCols.length > 0) {
|
||||||
|
console.log('Renaming service.description_pt to description_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `service` CHANGE COLUMN `description_pt` `description_es` TEXT"
|
||||||
|
);
|
||||||
|
console.log('✓ service.description_pt renamed to description_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ service.description_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and rename notification fields
|
||||||
|
console.log('\nChecking notification table...');
|
||||||
|
const [notifTitleCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `notification` LIKE 'title_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notifTitleCols.length > 0) {
|
||||||
|
console.log('Renaming notification.title_pt to title_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `notification` CHANGE COLUMN `title_pt` `title_es` VARCHAR(200) NOT NULL"
|
||||||
|
);
|
||||||
|
console.log('✓ notification.title_pt renamed to title_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ notification.title_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [notifContentCols] = await sequelize.query(
|
||||||
|
"SHOW COLUMNS FROM `notification` LIKE 'content_pt'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notifContentCols.length > 0) {
|
||||||
|
console.log('Renaming notification.content_pt to content_es...');
|
||||||
|
await sequelize.query(
|
||||||
|
"ALTER TABLE `notification` CHANGE COLUMN `content_pt` `content_es` TEXT"
|
||||||
|
);
|
||||||
|
console.log('✓ notification.content_pt renamed to content_es');
|
||||||
|
} else {
|
||||||
|
console.log('✓ notification.content_es already exists, skipping...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user language preferences
|
||||||
|
console.log('\nUpdating user language preferences...');
|
||||||
|
const [result] = await sequelize.query(
|
||||||
|
"UPDATE `user` SET language = 'es' WHERE language = 'pt'"
|
||||||
|
);
|
||||||
|
console.log(`✓ Updated ${result.affectedRows || 0} users from 'pt' to 'es'`);
|
||||||
|
|
||||||
|
console.log('\n========================================');
|
||||||
|
console.log('Migration completed successfully!');
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\nMigration failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
@ -4,6 +4,7 @@ const LoginHistory = require('../models/LoginHistory');
|
||||||
const Invitation = require('../models/Invitation');
|
const Invitation = require('../models/Invitation');
|
||||||
const { generateToken, generateRefreshToken } = require('../utils/jwt');
|
const { generateToken, generateRefreshToken } = require('../utils/jwt');
|
||||||
const env = require('../config/env');
|
const env = require('../config/env');
|
||||||
|
const configService = require('./configService');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authentication Service
|
* Authentication Service
|
||||||
|
|
@ -64,11 +65,22 @@ const wechatLogin = async (code, userInfo = {}, invitationCode = null, deviceInf
|
||||||
let user = await User.findOne({ where: { wechatOpenId: openId } });
|
let user = await User.findOne({ where: { wechatOpenId: openId } });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
// Get default avatar from config
|
||||||
|
let defaultAvatar = null;
|
||||||
|
try {
|
||||||
|
const avatarConfig = await configService.getConfig('default_avatar');
|
||||||
|
if (avatarConfig && avatarConfig.value) {
|
||||||
|
defaultAvatar = avatarConfig.value;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get default avatar config:', e);
|
||||||
|
}
|
||||||
|
|
||||||
// Create new user
|
// Create new user
|
||||||
const userData = {
|
const userData = {
|
||||||
wechatOpenId: openId,
|
wechatOpenId: openId,
|
||||||
nickname: userInfo.nickname || 'User',
|
nickname: userInfo.nickname || 'User',
|
||||||
avatar: userInfo.avatar || null,
|
avatar: userInfo.avatar || defaultAvatar,
|
||||||
invitationCode: await generateUniqueInvitationCode(),
|
invitationCode: await generateUniqueInvitationCode(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ const getPublicConfigs = async () => {
|
||||||
const configs = await getAllConfigs();
|
const configs = await getAllConfigs();
|
||||||
|
|
||||||
// Filter only public configurations
|
// Filter only public configurations
|
||||||
const publicKeys = ['app_logo', 'about_us_image', 'app_name', 'contact_phone', 'contact_email', 'contact_qr_image', 'user_agreement', 'privacy_policy', 'invite_rules'];
|
const publicKeys = ['app_logo', 'about_us_image', 'app_name', 'contact_phone', 'contact_email', 'contact_qr_image', 'user_agreement', 'privacy_policy', 'invite_rules', 'default_avatar'];
|
||||||
|
|
||||||
const publicConfigs = {};
|
const publicConfigs = {};
|
||||||
configs.forEach(config => {
|
configs.forEach(config => {
|
||||||
|
|
@ -140,6 +140,7 @@ const initializeDefaults = async () => {
|
||||||
const defaults = [
|
const defaults = [
|
||||||
{ key: 'app_logo', value: '', type: 'image', category: 'appearance', description: 'Application logo image URL' },
|
{ key: 'app_logo', value: '', type: 'image', category: 'appearance', description: 'Application logo image URL' },
|
||||||
{ key: 'about_us_image', value: '', type: 'image', category: 'appearance', description: 'About us section image URL' },
|
{ key: 'about_us_image', value: '', type: 'image', category: 'appearance', description: 'About us section image URL' },
|
||||||
|
{ key: 'default_avatar', value: '', type: 'image', category: 'appearance', description: '用户默认头像' },
|
||||||
{ key: 'app_name', value: 'Overseas Appointment System', type: 'string', category: 'general', description: 'Application name' },
|
{ key: 'app_name', value: 'Overseas Appointment System', type: 'string', category: 'general', description: 'Application name' },
|
||||||
{ key: 'contact_phone', value: '', type: 'string', category: 'contact', description: 'Contact phone number' },
|
{ key: 'contact_phone', value: '', type: 'string', category: 'contact', description: 'Contact phone number' },
|
||||||
{ key: 'contact_email', value: '', type: 'string', category: 'contact', description: 'Contact email address' },
|
{ key: 'contact_email', value: '', type: 'string', category: 'contact', description: 'Contact email address' },
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ var Config = Config || {}
|
||||||
|
|
||||||
// API 基础地址
|
// API 基础地址
|
||||||
// 注意:微信小程序开发工具无法访问localhost,需要使用本机IP地址
|
// 注意:微信小程序开发工具无法访问localhost,需要使用本机IP地址
|
||||||
// Config.API_BASE_URL = 'https://sub.zpc-xy.com' // 本地开发环境(使用本机IP)
|
Config.API_BASE_URL = 'https://sub.zpc-xy.com' // 本地开发环境(使用本机IP)
|
||||||
Config.API_BASE_URL = 'http://localhost:3000' // 本地开发环境(浏览器可用)
|
// Config.API_BASE_URL = 'http://localhost:3000' // 本地开发环境(浏览器可用)
|
||||||
// Config.API_BASE_URL = 'https://your-production-domain.com' // 生产环境(待配置)
|
// Config.API_BASE_URL = 'https://your-production-domain.com' // 生产环境(待配置)
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user