leiyun преди 3 седмици
родител
ревизия
c804dad4ea
променени са 25 файла, в които са добавени 791 реда и са изтрити 115 реда
  1. +2
    -1
      sql/update_20260224_spider_news.sql
  2. +23
    -0
      sql/update_20260225_spider_to_article.sql
  3. +4
    -0
      sql/update_20260225_spider_type.sql
  4. +12
    -0
      sql/update_20260227_column_extend.sql
  5. +2
    -1
      src/config/adapter.js
  6. +5
    -0
      src/config/router.js
  7. +0
    -9
      src/controller/admin/dashboard.js
  8. +83
    -11
      src/controller/admin/spider.js
  9. +6
    -0
      src/controller/admin/system/column.js
  10. +4
    -2
      src/controller/base.js
  11. +112
    -0
      src/controller/column.js
  12. +85
    -56
      src/controller/index.js
  13. +2
    -2
      view/admin/auth_login.html
  14. +51
    -4
      view/admin/system/column_index.html
  15. +26
    -0
      view/column/_article.html
  16. +19
    -0
      view/column/_image.html
  17. +16
    -0
      view/column/_job.html
  18. +8
    -0
      view/column/_page.html
  19. +24
    -0
      view/column/_person.html
  20. +19
    -0
      view/column/_text.html
  21. +71
    -0
      view/column_index.html
  22. +1
    -1
      view/common/_footer.html
  23. +6
    -6
      view/common/_header.html
  24. +13
    -9
      view/index_index.html
  25. +197
    -13
      www/static/css/web.css

+ 2
- 1
sql/update_20260224_spider_news.sql Целия файл

@@ -2,6 +2,7 @@
CREATE TABLE IF NOT EXISTS `pap_spider_news` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`origin_id` int(11) NOT NULL DEFAULT 0 COMMENT '原站文章ID',
`type` varchar(20) NOT NULL DEFAULT 'news' COMMENT '来源类型 news/about',
`title` varchar(500) NOT NULL DEFAULT '' COMMENT '文章标题',
`category` varchar(100) DEFAULT '' COMMENT '原站栏目分类',
`publish_time` datetime DEFAULT NULL COMMENT '原站发布时间',
@@ -16,6 +17,6 @@ CREATE TABLE IF NOT EXISTS `pap_spider_news` (
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_origin_id` (`origin_id`),
UNIQUE KEY `uk_type_origin_id` (`type`, `origin_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='爬虫-新闻数据';

+ 23
- 0
sql/update_20260225_spider_to_article.sql Целия файл

@@ -0,0 +1,23 @@
-- 将 pap_spider_news 中 id=119~126 的公益项目数据转入 pap_article,column_id=5
-- content 优先取已转存的 content,其次取原始 content_original
INSERT INTO `pap_article` (`column_id`, `title`, `summary`, `content`, `cover`, `category`, `is_top`, `status`, `publish_time`, `sort`, `create_time`)
SELECT
5 AS column_id,
s.title,
'' AS summary,
COALESCE(s.content, s.content_original, '') AS content,
COALESCE(s.cover, s.cover_original, '') AS cover,
s.category,
0 AS is_top,
1 AS status,
s.publish_time,
0 AS sort,
NOW()
FROM `pap_spider_news` s
WHERE s.id IN (119, 120, 121, 122, 123, 124, 125, 126);

-- 回写 article_id 到 spider_news(方便追溯)
UPDATE `pap_spider_news` sn
INNER JOIN `pap_article` a ON a.title = sn.title AND a.column_id = 5
SET sn.article_id = a.id, sn.status = 3
WHERE sn.id IN (119, 120, 121, 122, 123, 124, 125, 126);

+ 4
- 0
sql/update_20260225_spider_type.sql Целия файл

@@ -0,0 +1,4 @@
-- 给spider_news表添加type字段,区分news和about
ALTER TABLE `pap_spider_news` ADD COLUMN `type` varchar(20) NOT NULL DEFAULT 'news' COMMENT '来源类型 news/about' AFTER `origin_id`;
ALTER TABLE `pap_spider_news` DROP INDEX `uk_origin_id`;
ALTER TABLE `pap_spider_news` ADD UNIQUE KEY `uk_type_origin_id` (`type`, `origin_id`);

+ 12
- 0
sql/update_20260227_column_extend.sql Целия файл

@@ -0,0 +1,12 @@
-- 栏目表扩展:增加前台展示字段
ALTER TABLE `pap_column` ADD COLUMN `name_en` varchar(200) DEFAULT '' COMMENT '英文名称' AFTER `name`;
ALTER TABLE `pap_column` ADD COLUMN `description` varchar(500) DEFAULT '' COMMENT '栏目描述/副标题' AFTER `name_en`;
ALTER TABLE `pap_column` ADD COLUMN `banner_image` varchar(500) DEFAULT '' COMMENT '栏目Banner背景图' AFTER `description`;

-- 更新现有一级栏目数据
UPDATE `pap_column` SET name_en = 'ABOUT US', description = '苍穹 笃行 创新 守止' WHERE `key` = 'about';
UPDATE `pap_column` SET name_en = 'PUBLIC WELFARE PROJECTS', description = '聚焦健康公益,守护每一份希望' WHERE `key` = 'project';
UPDATE `pap_column` SET name_en = 'PARTY BUILDING', description = '不忘初心 牢记使命' WHERE `key` = 'party';
UPDATE `pap_column` SET name_en = 'INFORMATION DISCLOSURE', description = '公开透明 接受监督' WHERE `key` = 'disclosure';
UPDATE `pap_column` SET name_en = 'NEWS CENTER', description = '了解基金会最新动态' WHERE `key` = 'news';
UPDATE `pap_column` SET name_en = 'CONTACT US', description = '期待与您携手同行' WHERE `key` = 'contact';

+ 2
- 1
src/config/adapter.js Целия файл

@@ -44,7 +44,8 @@ exports.model = {
port: '26821',
user: 'root',
password: '5orKUdDN3QhESVcS',
dateStrings: true
dateStrings: true,
connectionLimit: 10
}
};



+ 5
- 0
src/config/router.js Целия файл

@@ -3,6 +3,10 @@ module.exports = [
['/', 'index/index'],
['/index', 'index/index'],

// 一级栏目页面(带子栏目)
['/column/:key', 'column/index', 'get'],
['/column/:key/:child', 'column/index', 'get'],

// 后台管理路由
['/admin/login', 'admin/auth/login', 'get'],
['/admin/auth/login', 'admin/auth/doLogin', 'post'],
@@ -110,6 +114,7 @@ module.exports = [

// 爬虫工具
['/admin/spider/fetch-list', 'admin/spider/fetchList'],
['/admin/spider/fetch-about-list', 'admin/spider/fetchAboutList'],
['/admin/spider/fetch-detail', 'admin/spider/fetchDetail'],
['/admin/spider/transfer-images', 'admin/spider/transferImages'],
['/admin/spider/sync', 'admin/spider/sync'],


+ 0
- 9
src/controller/admin/dashboard.js Целия файл

@@ -20,18 +20,9 @@ module.exports = class extends Base {
// 待办事项(后续从数据库获取)
this.assign('todoList', []);

// 动态栏目菜单(后续从接口获取)
this.assign('columnMenus', await this.getColumnMenus());

// 当前登录用户
this.assign('adminUser', this.adminUser || {});

return this.display();
}

// 获取动态栏目菜单(暂时返回空,后续从数据库获取)
async getColumnMenus() {
// TODO: 从数据库获取栏目配置
return [];
}
};

+ 83
- 11
src/controller/admin/spider.js Целия файл

@@ -26,7 +26,7 @@ module.exports = class extends Base {
async fetchListAction() {
try {
const res = await axios.post(
`${ORIGIN.listUrl}?act=contentlist&id=&type=news`,
`${ORIGIN.listUrl}?act=contentlist&id=&type=product`,
'order=asc&limit=9999&offset=0',
{
headers: {
@@ -53,7 +53,7 @@ module.exports = class extends Base {
const publishTime = this.extractTime(row.c_addtime);

// 检查是否已存在
const exists = await model.where({ origin_id: originId }).find();
const exists = await model.where({ origin_id: originId, type: 'news' }).find();
if (exists && exists.id) {
skipped++;
continue;
@@ -64,6 +64,7 @@ module.exports = class extends Base {
title,
category,
publish_time: publishTime,
type: 'product',
status: 0
});
inserted++;
@@ -80,6 +81,67 @@ module.exports = class extends Base {
}
}

/**
* 步骤1b:拉取about列表并入库
* GET /admin/spider/fetch-about-list
* 参数: sid (可选,指定栏目sid)
*/
async fetchAboutListAction() {
try {
const sid = this.get('sid') || '';
const res = await axios.post(
`${ORIGIN.listUrl}?act=aboutlist&sid=${sid}`,
'order=asc&limit=9999&offset=0',
{
headers: {
Cookie: ORIGIN.cookie,
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);

const rows = res.data;
if (!rows || !Array.isArray(rows)) {
return this.json({ code: 1, msg: 'about列表数据获取失败' });
}

const model = this.model('spider_news');
let inserted = 0;
let skipped = 0;

for (const row of rows) {
const originId = parseInt(row.id);
const title = this.extractTitle(row.a_name);
const category = row.sid || '';

// 检查是否已存在
const exists = await model.where({ origin_id: originId, type: 'about' }).find();
if (exists && exists.id) {
skipped++;
continue;
}

await model.add({
origin_id: originId,
title,
category,
type: 'about',
status: 0
});
inserted++;
}

return this.json({
code: 0,
msg: `about列表拉取完成,新增${inserted}条,跳过${skipped}条`,
data: { total: rows.length, inserted, skipped }
});
} catch (err) {
think.logger.error('拉取about列表失败:', err);
return this.json({ code: 1, msg: '拉取about列表失败: ' + err.message });
}
}

/**
* 步骤2:爬取详情页内容
* GET /admin/spider/fetch-detail
@@ -87,14 +149,19 @@ module.exports = class extends Base {
*/
async fetchDetailAction() {
const targetId = this.get('id');
const type = this.get('type') || '';
const model = this.model('spider_news');

let list;
let where = { status: 0 };
if (targetId) {
list = await model.where({ origin_id: targetId }).select();
} else {
list = await model.where({ status: 0 }).select();
where = { origin_id: targetId };
if (type) where.type = type;
}
if (type && !targetId) {
where.type = type;
}

const list = await model.where(where).select();

if (!list.length) {
return this.json({ code: 0, msg: '没有待爬取的记录' });
@@ -105,7 +172,7 @@ module.exports = class extends Base {

for (const item of list) {
try {
const detailUrl = `${ORIGIN.base}/?news/${item.origin_id}.html`;
const detailUrl = `${ORIGIN.base}/?${type}/${item.origin_id}.html`
const res = await axios.get(detailUrl, {
headers: { Cookie: ORIGIN.cookie },
responseType: 'arraybuffer'
@@ -168,15 +235,20 @@ module.exports = class extends Base {
*/
async transferImagesAction() {
const targetId = this.get('id');
const type = this.get('type') || '';
const model = this.model('spider_news');

let list;
let where = { status: 1 };
if (targetId) {
list = await model.where({ origin_id: targetId }).select();
} else {
list = await model.where({ status: 1 }).select();
where = { origin_id: targetId };
if (type) where.type = type;
}
if (type && !targetId) {
where.type = type;
}

const list = await model.where(where).select();

if (!list.length) {
return this.json({ code: 0, msg: '没有待转存图片的记录' });
}


+ 6
- 0
src/controller/admin/system/column.js Целия файл

@@ -54,6 +54,9 @@ module.exports = class extends Base {
const id = await model.add({
parent_id: data.parent_id || 0,
name: data.name,
name_en: data.name_en || '',
description: data.description || '',
banner_image: data.banner_image || '',
key: data.key || '',
icon: data.icon || '',
type: data.type || '',
@@ -88,6 +91,9 @@ module.exports = class extends Base {
await this.model('column').where({ id }).update({
parent_id: data.parent_id || 0,
name: data.name,
name_en: data.name_en || '',
description: data.description || '',
banner_image: data.banner_image || '',
key: data.key || '',
icon: data.icon || '',
type: data.type || '',


+ 4
- 2
src/controller/base.js Целия файл

@@ -12,6 +12,7 @@ const WHITE_LIST = [
'/admin/logout',
// 爬虫工具
'/admin/spider/fetch-list',
'/admin/spider/fetch-about-list',
'/admin/spider/fetch-detail',
'/admin/spider/transfer-images',
'/admin/spider/sync',
@@ -49,8 +50,9 @@ module.exports = class extends think.Controller {
}
}

// 白名单放行
if (WHITE_LIST.includes(path)) {
// 白名单放行(同时匹配带.html后缀的路径)
const pathNoSuffix = path.replace(/\.html$/, '');
if (WHITE_LIST.includes(path) || WHITE_LIST.includes(pathNoSuffix)) {
return;
}



+ 112
- 0
src/controller/column.js Целия файл

@@ -0,0 +1,112 @@
const Base = require('./base.js');

module.exports = class extends Base {
async indexAction() {
const columnKey = this.get('key');
const childKey = this.get('child');

if (!columnKey) return this.redirect('/');

// 获取所有栏目
const columns = await this.model('column').where({ is_deleted: 0, visible: 1 }).order('sort ASC').select();
const navMenus = this.buildNavTree(columns);

// 找到当前一级栏目
const column = columns.find(c => c.key === columnKey && c.parent_id === 0);
if (!column) return this.redirect('/');

// 获取子栏目
const children = columns.filter(c => c.parent_id === column.id).sort((a, b) => a.sort - b.sort);

// 确定当前选中的子栏目
let currentChild = null;
if (childKey) {
currentChild = children.find(c => c.key === childKey);
}
if (!currentChild && children.length > 0) {
currentChild = children[0];
}

// 根据子栏目类型加载内容
let contentData = {};
if (currentChild) {
contentData = await this.loadContent(currentChild);
}

// 网站配置
const configList = await this.model('config').select();
const siteConfig = {};
configList.forEach(c => { siteConfig[c.key] = c.value; });

this.assign({
navMenus,
column,
children,
currentChild,
contentData,
siteConfig,
currentColumnKey: columnKey
});

return this.display('column_index');
}

// 根据类型加载内容
async loadContent(child) {
const colId = child.id;
const type = child.type;

switch (type) {
case 'page': {
const page = await this.model('page').where({ column_id: colId, is_deleted: 0 }).find();
return { type: 'page', page: page || {} };
}
case 'article': {
const list = await this.model('article').where({ column_id: colId, status: 1, is_deleted: 0 })
.order('is_top DESC, sort DESC, publish_time DESC').limit(20).select();
// 格式化日期
list.forEach(item => {
if (item.publish_time) {
const d = new Date(item.publish_time);
item.publish_date = d.getFullYear() + '.' + String(d.getMonth() + 1).padStart(2, '0') + '.' + String(d.getDate()).padStart(2, '0');
}
});
return { type: 'article', list };
}
case 'image': {
const list = await this.model('image').where({ column_id: colId, status: 1, is_deleted: 0 })
.order('sort ASC').select();
return { type: 'image', list };
}
case 'text': {
const list = await this.model('text').where({ column_id: colId, status: 1, is_deleted: 0 })
.order('sort ASC, id DESC').select();
return { type: 'text', list };
}
case 'person': {
const list = await this.model('person').where({ column_id: colId, status: 1, is_deleted: 0 })
.order('sort ASC').select();
return { type: 'person', list };
}
case 'job': {
const list = await this.model('job').where({ column_id: colId, status: 1, is_deleted: 0 })
.order('sort ASC, id DESC').select();
return { type: 'job', list };
}
default:
return { type: type || 'empty' };
}
}

// 构建导航树
buildNavTree(columns) {
const tree = [];
const map = {};
columns.forEach(item => { map[item.id] = { ...item, children: [] }; });
columns.forEach(item => {
if (item.parent_id === 0) tree.push(map[item.id]);
else if (map[item.parent_id]) map[item.parent_id].children.push(map[item.id]);
});
return tree;
}
};

+ 85
- 56
src/controller/index.js Целия файл

@@ -1,10 +1,26 @@
const Base = require('./base.js');

// 首页数据内存缓存
let homeCache = null;
let homeCacheTime = 0;
const CACHE_TTL = 60 * 1000; // 60秒缓存

module.exports = class extends Base {
async indexAction() {
const t0 = Date.now();

// 检查缓存
if (homeCache && (Date.now() - homeCacheTime < CACHE_TTL)) {
console.log(`[perf] cache hit, age: ${Date.now() - homeCacheTime}ms`);
this.assign(homeCache);
return this.display();
}

// 获取栏目配置
const columns = await this.model('column').where({ is_deleted: 0, visible: 1 }).order('sort ASC').select();
const t1 = Date.now();
console.log(`[perf] columns: ${t1 - t0}ms`);

// 构建导航菜单树
const navMenus = this.buildNavTree(columns);
@@ -14,71 +30,76 @@ module.exports = class extends Base {
const colMap = {};
homeChildren.forEach(c => { colMap[c.key] = c.id; });
// 获取Banner数据(根据栏目key查找column_id,或者column_id=0表示通用)
const bannerColId = colMap['home-banner'] || 0;
const banners = await this.model('banner').where({
status: 1,
is_deleted: 0
}).where('column_id = ' + bannerColId + ' OR column_id = 0').order('sort ASC').select();
// 获取捐赠统计
const dataColId = colMap['home-data'] || 0;
let donationStat = await this.model('donation_stat').where({
column_id: dataColId
}).find();
const projectColId = colMap['home-project'] || 0;
const newsColId = colMap['home-news'] || 0;
const partnerColId = colMap['home-partner'] || 0;

const t2 = Date.now();
// 并行查询所有数据
const [
banners,
donationStatResult,
donationIncome,
donationExpense,
medicines,
projects,
newsList,
partners,
configList
] = await Promise.all([
this.model('banner').where({ status: 1, is_deleted: 0 })
.where('column_id = ' + bannerColId + ' OR column_id = 0').order('sort ASC').select(),
this.model('donation_stat').where({ column_id: dataColId }).find(),
this.model('donation').where({ type: 'income', status: 1, is_deleted: 0 })
.where('column_id = ' + dataColId + ' OR column_id = 0').order('record_date DESC').limit(10).select(),
this.model('donation').where({ type: 'expense', status: 1, is_deleted: 0 })
.where('column_id = ' + dataColId + ' OR column_id = 0').order('record_date DESC').limit(10).select(),
this.model('medicine').where({ status: 1, is_deleted: 0 })
.order('distribute_date DESC').limit(20).select(),
this.model('article').where({ status: 1, is_deleted: 0 })
.where('column_id = ' + projectColId + ' OR column_id = 0').order('sort DESC, publish_time DESC').limit(5).select(),
this.model('article').where({ status: 1, is_deleted: 0 })
.where('column_id = ' + newsColId + ' OR column_id = 0').order('is_top DESC, publish_time DESC').limit(5).select(),
this.model('image').where({ status: 1, is_deleted: 0 })
.where('column_id = ' + partnerColId + ' OR column_id = 0').order('sort ASC').select(),
this.model('config').select()
]);
const t3 = Date.now();
console.log(`[perf] parallel queries: ${t3 - t2}ms`);

// 捐赠统计兜底
let donationStat = donationStatResult;
if (!donationStat || !donationStat.id) {
donationStat = await this.model('donation_stat').where({ column_id: 0 }).find();
}
donationStat = donationStat || { total_income: 0, total_expense: 0, year_income: 0 };
// 获取捐赠明细(收入/支出各取最新10条)
const donationIncome = await this.model('donation').where({
type: 'income',
status: 1,
is_deleted: 0
}).where('column_id = ' + dataColId + ' OR column_id = 0').order('record_date DESC').limit(10).select();
const donationExpense = await this.model('donation').where({
type: 'expense',
status: 1,
is_deleted: 0
}).where('column_id = ' + dataColId + ' OR column_id = 0').order('record_date DESC').limit(10).select();
// 获取药品援助数据(只显示已发放的)
const medicines = await this.model('medicine').where({
status: 1,
is_deleted: 0
}).order('distribute_date DESC').limit(20).select();
// 获取公益项目
const projectColId = colMap['home-project'] || 0;
const projects = await this.model('article').where({
status: 1,
is_deleted: 0
}).where('column_id = ' + projectColId + ' OR column_id = 0').order('sort DESC, publish_time DESC').limit(5).select();
// 获取新闻动态
const newsColId = colMap['home-news'] || 0;
const newsList = await this.model('article').where({
status: 1,
is_deleted: 0
}).where('column_id = ' + newsColId + ' OR column_id = 0').order('is_top DESC, publish_time DESC').limit(5).select();
// 获取合作伙伴
const partnerColId = colMap['home-partner'] || 0;
const partners = await this.model('image').where({
status: 1,
is_deleted: 0
}).where('column_id = ' + partnerColId + ' OR column_id = 0').order('sort ASC').select();
// 获取网站配置
const configList = await this.model('config').select();

// 格式化日期
const formatDate = (str) => {
if (!str) return { date: '', day: '', ym: '' };
const d = new Date(str);
if (isNaN(d.getTime())) return { date: '', day: '', ym: '' };
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return { date: y + '.' + m + '.' + dd, day: dd, ym: y + '.' + m };
};
newsList.forEach(item => {
const fd = formatDate(item.publish_time);
item.publish_date = fd.date;
item.publish_day = fd.day;
item.publish_ym = fd.ym;
});

// 网站配置
const siteConfig = {};
configList.forEach(c => {
siteConfig[c.key] = c.value;
});
this.assign({
const data = {
navMenus,
banners,
donationStat,
@@ -90,8 +111,16 @@ module.exports = class extends Base {
partners,
siteConfig,
now: new Date()
});
};

// 写入缓存
homeCache = data;
homeCacheTime = Date.now();

this.assign(data);
const t4 = Date.now();
console.log(`[perf] TOTAL: ${t4 - t0}ms`);
return this.display();
}


+ 2
- 2
view/admin/auth_login.html Целия файл

@@ -199,10 +199,10 @@ document.getElementById('loginForm').addEventListener('submit', async function(e
body: JSON.stringify({ username, password, remember })
});
const data = await res.json();
if (data.errno === 0) {
if (data.code === 0) {
window.location.href = '/admin/dashboard.html';
} else {
errorMsg.textContent = data.errmsg || '登录失败';
errorMsg.textContent = data.msg || '登录失败';
errorMsg.classList.add('show');
}
} catch (err) {


+ 51
- 4
view/admin/system/column_index.html Целия файл

@@ -46,6 +46,19 @@
<el-form-item label="栏目名称" required>
<el-input v-model="form.name" placeholder="请输入栏目名称"></el-input>
</el-form-item>
<el-form-item label="英文名称" v-if="form.parent_id === 0">
<el-input v-model="form.name_en" placeholder="如: ABOUT US"></el-input>
</el-form-item>
<el-form-item label="栏目描述" v-if="form.parent_id === 0">
<el-input v-model="form.description" placeholder="一句话描述"></el-input>
</el-form-item>
<el-form-item label="Banner图" v-if="form.parent_id === 0">
<div class="flex items-center gap-3">
<el-image v-if="form.banner_image" :src="form.banner_image" style="width:200px;height:80px;border-radius:4px;" fit="cover"></el-image>
<el-button @click="uploadBanner">上传图片</el-button>
<el-button v-if="form.banner_image" link type="danger" @click="form.banner_image = ''">清除</el-button>
</div>
</el-form-item>
<el-form-item label="栏目标识">
<el-input v-model="form.key" placeholder="如: about-us(用于路由)"></el-input>
</el-form-item>
@@ -112,6 +125,19 @@
<el-form-item label="栏目名称" required>
<el-input v-model="form.name" placeholder="请输入栏目名称"></el-input>
</el-form-item>
<el-form-item label="英文名称" v-if="form.parent_id === 0">
<el-input v-model="form.name_en" placeholder="如: ABOUT US"></el-input>
</el-form-item>
<el-form-item label="栏目描述" v-if="form.parent_id === 0">
<el-input v-model="form.description" placeholder="一句话描述"></el-input>
</el-form-item>
<el-form-item label="Banner图" v-if="form.parent_id === 0">
<div class="flex items-center gap-3">
<el-image v-if="form.banner_image" :src="form.banner_image" style="width:200px;height:80px;border-radius:4px;" fit="cover"></el-image>
<el-button @click="uploadBanner">上传图片</el-button>
<el-button v-if="form.banner_image" link type="danger" @click="form.banner_image = ''">清除</el-button>
</div>
</el-form-item>
<el-form-item label="栏目标识">
<el-input v-model="form.key" placeholder="如: about-us(用于路由)"></el-input>
</el-form-item>
@@ -288,7 +314,7 @@ const app = createApp({
const saving = ref(false);
const parentIsSinglePage = ref(false);
const form = reactive({
id: null, parent_id: 0, name: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
id: null, parent_id: 0, name: '', name_en: '', description: '', banner_image: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
link: '', seo_title: '', seo_keywords: '', seo_description: '', slug: ''
});

@@ -304,6 +330,25 @@ const app = createApp({
iconPickerVisible.value = false;
}

// Banner图片上传
function uploadBanner() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const fd = new FormData();
fd.append('file', file);
try {
const res = await fetch('/admin/upload', { method: 'POST', body: fd }).then(r => r.json());
if (res.code === 0) { form.banner_image = res.data.url; }
else { ElementPlus.ElMessage.error(res.msg || '上传失败'); }
} catch { ElementPlus.ElMessage.error('上传失败'); }
};
input.click();
}

const showSeoTab = computed(() => {
if (form.parent_id === 0 && form.is_single_page === 1) return true;
if (form.parent_id !== 0 && !parentIsSinglePage.value) return true;
@@ -338,7 +383,7 @@ const app = createApp({
activeTab.value = 'basic';
parentIsSinglePage.value = parentData ? !!parentData.is_single_page : false;
Object.assign(form, {
id: null, parent_id: parentId || 0, name: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
id: null, parent_id: parentId || 0, name: '', name_en: '', description: '', banner_image: '', key: '', icon: '', type: '', is_single_page: 0, sort: 1, visible: 1,
link: '', seo_title: '', seo_keywords: '', seo_description: '', slug: ''
});
dialogVisible.value = true;
@@ -354,7 +399,8 @@ const app = createApp({
parentIsSinglePage.value = false;
}
Object.assign(form, {
id: row.id, parent_id: row.parent_id || 0, name: row.name, key: row.key || '', icon: row.icon || '',
id: row.id, parent_id: row.parent_id || 0, name: row.name, name_en: row.name_en || '', description: row.description || '',
banner_image: row.banner_image || '', key: row.key || '', icon: row.icon || '',
type: row.type || '', is_single_page: row.is_single_page || 0, sort: row.sort || 1, visible: row.visible,
link: row.link || '', seo_title: row.seo_title || '', seo_keywords: row.seo_keywords || '',
seo_description: row.seo_description || '', slug: row.slug || ''
@@ -412,7 +458,8 @@ const app = createApp({
iconPickerVisible, iconSearch, filteredIcons, selectIcon,
formDrawerVisible, formDrawerTitle, formFields, formSaving,
loadList, toggleAllTree, showAddModal, editItem, saveItem, deleteItem,
openFormConfig, addFormField, moveField, removeField, saveFormConfig
openFormConfig, addFormField, moveField, removeField, saveFormConfig,
uploadBanner
};
}
});


+ 26
- 0
view/column/_article.html Целия файл

@@ -0,0 +1,26 @@
<!-- 图文列表 -->
<div class="col-article-list">
{% if contentData.list and contentData.list.length > 0 %}
{% for item in contentData.list %}
<a href="/article/{{item.id}}.html" class="col-article-item">
{% if item.cover %}
<div class="col-article-cover">
<img src="{{item.cover}}" alt="{{item.title}}">
</div>
{% endif %}
<div class="col-article-info">
<h3>{{item.title}}</h3>
{% if item.summary %}
<p>{{item.summary}}</p>
{% endif %}
<div class="col-article-meta">
{% if item.publish_date %}<span class="date">{{item.publish_date}}</span>{% endif %}
{% if item.category %}<span class="cate">{{item.category}}</span>{% endif %}
</div>
</div>
</a>
{% endfor %}
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>

+ 19
- 0
view/column/_image.html Целия файл

@@ -0,0 +1,19 @@
<!-- 图片列表 -->
<div class="col-image-grid">
{% if contentData.list and contentData.list.length > 0 %}
{% for item in contentData.list %}
<div class="col-image-item">
<div class="col-image-thumb">
{% if item.link %}
<a href="{{item.link}}" target="_blank"><img src="{{item.image}}" alt="{{item.title}}"></a>
{% else %}
<img src="{{item.image}}" alt="{{item.title}}">
{% endif %}
</div>
{% if item.title %}<div class="col-image-title">{{item.title}}</div>{% endif %}
</div>
{% endfor %}
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>

+ 16
- 0
view/column/_job.html Целия файл

@@ -0,0 +1,16 @@
<!-- 岗位列表 -->
<div class="col-job-list">
{% if contentData.list and contentData.list.length > 0 %}
{% for item in contentData.list %}
<div class="col-job-item">
<div class="col-job-header">
<h3>{{item.title}}</h3>
{% if item.location %}<span class="col-job-loc">{{item.location}}</span>{% endif %}
</div>
{% if item.description %}<p class="col-job-desc">{{item.description}}</p>{% endif %}
</div>
{% endfor %}
{% else %}
<div class="col-empty">暂无招聘信息</div>
{% endif %}
</div>

+ 8
- 0
view/column/_page.html Целия файл

@@ -0,0 +1,8 @@
<!-- 单页内容 -->
<div class="col-page">
{% if contentData.page and contentData.page.content %}
<div class="col-page-body">{{contentData.page.content | safe}}</div>
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>

+ 24
- 0
view/column/_person.html Целия файл

@@ -0,0 +1,24 @@
<!-- 人员列表 -->
<div class="col-person-grid">
{% if contentData.list and contentData.list.length > 0 %}
{% for item in contentData.list %}
<div class="col-person-card">
<div class="col-person-photo">
{% if item.avatar %}
<img src="{{item.avatar}}" alt="{{item.name}}">
{% else %}
<div class="col-person-placeholder">{{item.name[0] | default('?')}}</div>
{% endif %}
</div>
<div class="col-person-info">
<div class="col-person-name">{{item.name}}</div>
{% if item.title %}<div class="col-person-title">{{item.title}}</div>{% endif %}
{% if item.description %}<div class="col-person-bio">{{item.description}}</div>{% endif %}
{% if item.category %}<div class="col-person-cate">{{item.category}}</div>{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>

+ 19
- 0
view/column/_text.html Целия файл

@@ -0,0 +1,19 @@
<!-- 文字列表 -->
<div class="col-text-list">
{% if contentData.list and contentData.list.length > 0 %}
{% for item in contentData.list %}
<div class="col-text-item">
<div class="col-text-title">
{% if item.attachment %}
<a href="{{item.attachment}}" target="_blank">{{item.title}}</a>
{% else %}
<span>{{item.title}}</span>
{% endif %}
</div>
{% if item.publish_date %}<div class="col-text-date">{{item.publish_date}}</div>{% endif %}
</div>
{% endfor %}
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>

+ 71
- 0
view/column_index.html Целия файл

@@ -0,0 +1,71 @@
{% extends "layout.html" %}

{% block title %}{{column.name}} - {{siteConfig.site_name}}{% endblock %}

{% block content %}
<!-- Column Banner -->
<div class="col-banner" {% if column.banner_image %}style="background-image:url('{{column.banner_image}}')"{% endif %}>
<div class="col-banner-overlay"></div>
<div class="col-banner-content">
<h1>{{column.name}}</h1>
{% if column.name_en %}
<div class="col-banner-en">{{column.name_en}}</div>
{% endif %}
{% if column.description %}
<div class="col-banner-desc">{{column.description}}</div>
{% endif %}
</div>
</div>

<!-- Sub Navigation Tabs -->
{% if children and children.length > 0 %}
<div class="col-tabs-wrap">
<div class="col-tabs">
{% for child in children %}
<a href="/column/{{column.key}}/{{child.key}}.html"
class="col-tab{% if currentChild and currentChild.key == child.key %} active{% endif %}">
{{child.name}}
</a>
{% endfor %}
</div>
</div>
{% endif %}

<!-- Breadcrumb -->
<div class="col-breadcrumb-wrap">
<div class="col-breadcrumb">
<span>您的位置:</span>
<a href="/">首页</a><span class="sep">&gt;</span>
<a href="/column/{{column.key}}.html">{{column.name}}</a>
{% if currentChild %}
<span class="sep">&gt;</span><span class="current">{{currentChild.name}}</span>
{% endif %}
</div>
</div>

<!-- Content Area -->
<div class="col-content-wrap">
<div class="col-content">
{% if currentChild %}
<h2 class="col-section-title">{{currentChild.name}}</h2>
{% endif %}
{% if contentData.type == 'page' %}
{% include "column/_page.html" %}
{% elif contentData.type == 'article' %}
{% include "column/_article.html" %}
{% elif contentData.type == 'image' %}
{% include "column/_image.html" %}
{% elif contentData.type == 'text' %}
{% include "column/_text.html" %}
{% elif contentData.type == 'person' %}
{% include "column/_person.html" %}
{% elif contentData.type == 'job' %}
{% include "column/_job.html" %}
{% else %}
<div class="col-empty">暂无内容</div>
{% endif %}
</div>
</div>

{% include "common/_footer.html" %}
{% endblock %}

+ 1
- 1
view/common/_footer.html Целия файл

@@ -35,7 +35,7 @@
<div class="footer-links">
{% for menu in navMenus %}
{% if loop.index <= 6 %}
<a href="{% if menu.key == 'home' %}/{% else %}/{{menu.key}}.html{% endif %}">{{menu.name}}</a>
<a href="{% if menu.key == 'home' %}/{% else %}/column/{{menu.key}}.html{% endif %}">{{menu.name}}</a>
{% if loop.index < 6 and loop.index < navMenus.length %}
<span class="separator">|</span>
{% endif %}


+ 6
- 6
view/common/_header.html Целия файл

@@ -9,15 +9,15 @@
{% for menu in navMenus %}
{% if menu.children and menu.children.length > 0 and not menu.is_single_page %}
<div class="nav-dropdown">
<a href="{% if menu.link %}{{menu.link}}{% else %}javascript:;{% endif %}" class="nav-link">{{menu.name}}</a>
<a href="/column/{{menu.key}}.html" class="nav-link">{{menu.name}}</a>
<div class="dropdown-menu">
{% for child in menu.children %}
<a href="/{{menu.key}}/{{child.key}}.html">{{child.name}}</a>
<a href="/column/{{menu.key}}/{{child.key}}.html">{{child.name}}</a>
{% endfor %}
</div>
</div>
{% else %}
<a href="{% if menu.key == 'home' %}/{% else %}/{{menu.key}}.html{% endif %}" class="nav-link{% if menu.key == 'home' %} active{% endif %}">{{menu.name}}</a>
<a href="{% if menu.key == 'home' %}/{% else %}/column/{{menu.key}}.html{% endif %}" class="nav-link{% if menu.key == 'home' %} active{% endif %}">{{menu.name}}</a>
{% endif %}
{% endfor %}
</nav>
@@ -44,19 +44,19 @@
{% for menu in navMenus %}
{% if menu.children and menu.children.length > 0 and not menu.is_single_page %}
<div class="mobile-nav-item has-children">
<a href="{% if menu.link %}{{menu.link}}{% else %}javascript:;{% endif %}" class="mobile-nav-link">
<a href="/column/{{menu.key}}.html" class="mobile-nav-link">
{{menu.name}}
<span class="mobile-nav-arrow">›</span>
</a>
<div class="mobile-nav-sub">
{% for child in menu.children %}
<a href="/{{menu.key}}/{{child.key}}.html">{{child.name}}</a>
<a href="/column/{{menu.key}}/{{child.key}}.html">{{child.name}}</a>
{% endfor %}
</div>
</div>
{% else %}
<div class="mobile-nav-item">
<a href="{% if menu.key == 'home' %}/{% else %}/{{menu.key}}.html{% endif %}" class="mobile-nav-link">{{menu.name}}</a>
<a href="{% if menu.key == 'home' %}/{% else %}/column/{{menu.key}}.html{% endif %}" class="mobile-nav-link">{{menu.name}}</a>
</div>
{% endif %}
{% endfor %}


+ 13
- 9
view/index_index.html Целия файл

@@ -77,12 +77,14 @@
<div class="dtable">
<h3>捐赠收入明细</h3>
<div class="dtable-table-wrap">
<table>
<thead><tr><th>捐赠方</th><th class="td-r" style="width:120px;">金额</th></tr></thead>
<table class="dtable-head">
<colgroup><col><col style="width:140px;"></colgroup>
<thead><tr><th>捐赠方</th><th class="td-r">金额</th></tr></thead>
</table>
<div class="dtable-scroll-wrap">
<div class="dtable-scroll-wrap donation-scroll">
<div class="dtable-scroll-inner">
<table>
<colgroup><col><col style="width:140px;"></colgroup>
<tbody>
{% for item in donationIncome %}
<tr><td>{{item.name}}</td><td class="td-r">{{item.amount}}</td></tr>
@@ -99,12 +101,14 @@
<div class="dtable">
<h3>公益支出明细</h3>
<div class="dtable-table-wrap">
<table>
<thead><tr><th>支付对象</th><th class="td-r" style="width:120px;">金额</th></tr></thead>
<table class="dtable-head">
<colgroup><col><col style="width:140px;"></colgroup>
<thead><tr><th>支付对象</th><th class="td-r">金额</th></tr></thead>
</table>
<div class="dtable-scroll-wrap">
<div class="dtable-scroll-wrap donation-scroll">
<div class="dtable-scroll-inner">
<table>
<colgroup><col><col style="width:140px;"></colgroup>
<tbody>
{% for item in donationExpense %}
<tr><td>{{item.name}}</td><td class="td-r">{{item.amount}}</td></tr>
@@ -198,7 +202,7 @@
<img src="{{newsList[0].cover}}" alt="{{newsList[0].title}}">
<div class="overlay">
<h3>{{newsList[0].title}}</h3>
<div class="date">{{newsList[0].publish_time}}</div>
<div class="date">{{newsList[0].publish_date}}</div>
</div>
</a>
{% endif %}
@@ -207,8 +211,8 @@
{% if loop.index > 1 %}
<a href="/news/detail/{{news.id}}.html" class="news-item">
<div class="date-box">
<div class="day">{{loop.index}}</div>
<div class="ym">{{news.publish_time}}</div>
<div class="day">{{news.publish_day}}</div>
<div class="ym">{{news.publish_ym}}</div>
</div>
<div class="text">
<h4>{{news.title}}</h4>


+ 197
- 13
www/static/css/web.css Целия файл

@@ -41,18 +41,18 @@ img{max-width:100%;display:block;}
position:relative;padding:8px 18px;font-size:15px;font-weight:500;
color:var(--gray-700);letter-spacing:1px;transition:.25s;border-radius:4px;
}
.nav-link:hover,.nav-link.active{color:var(--orange);background:rgba(232,117,26,.08);}
.nav-link:hover,.nav-link.active{color:var(--orange);}

/* Dropdown */
.nav-dropdown{position:relative;}
.nav-dropdown .dropdown-menu{
position:absolute;top:100%;left:50%;transform:translateX(-50%) translateY(8px);
position:absolute;top:100%;left:50%;transform:translateX(-50%) translateY(4px);
min-width:160px;background:rgba(255,255,255,.98);backdrop-filter:blur(12px);border-radius:8px;
box-shadow:0 8px 32px rgba(0,0,0,.12);padding:8px 0;border:1px solid rgba(0,0,0,.06);
opacity:0;visibility:hidden;transition:.25s;pointer-events:none;
}
.nav-dropdown:hover .dropdown-menu{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);pointer-events:auto;}
.dropdown-menu a{display:block;padding:10px 24px;font-size:14px;color:var(--gray-700);white-space:nowrap;transition:.15s;}
.dropdown-menu a{display:block;padding:10px 24px;font-size:14px;color:var(--gray-700);white-space:nowrap;transition:.15s;text-align:center;}
.dropdown-menu a:hover{color:var(--orange);background:rgba(232,117,26,.04);}

.header-donate{
@@ -265,11 +265,11 @@ img{max-width:100%;display:block;}
.dtable th{text-align:center;padding:5px 8px;color:var(--gray-500);font-weight:500;border-bottom:1px solid var(--gray-100);}
.dtable td{text-align:center;padding:5px 8px;color:var(--gray-700);border-bottom:1px solid var(--gray-100);}
.dtable .td-r{font-family:var(--font-en);font-weight:500;color:var(--orange);}
.dtable-head{margin-bottom:0;}
.dtable-scroll-wrap{max-height:140px;overflow:hidden;position:relative;}
.dtable-scroll-inner{animation:dtableScroll 12s cubic-bezier(.45,.05,.55,.95) infinite;}
.dtable:nth-child(2) .dtable-scroll-inner{animation-delay:1.5s;}
.dtable-scroll-wrap:hover .dtable-scroll-inner{animation-play-state:paused;}
@keyframes dtableScroll{0%,15%{transform:translateY(0);}50%,65%{transform:translateY(-50%);}100%{transform:translateY(0);}}
.dtable-scroll-wrap.donation-scroll .dtable-scroll-inner{animation:donationScroll 15s linear infinite;}
.dtable-scroll-wrap.donation-scroll:hover .dtable-scroll-inner{animation-play-state:paused;}
@keyframes donationScroll{0%{transform:translateY(0);}100%{transform:translateY(-50%);}}

.dtable-drug{grid-column:1/-1;}
.dtable-drug h3{border-bottom-color:var(--orange);}
@@ -354,9 +354,9 @@ img{max-width:100%;display:block;}
}
.swiper-slide-active .news-wrap{opacity:1;transform:translateY(0);}

.news-grid{display:grid;grid-template-columns:1fr 1fr;gap:36px;align-items:start;}
.news-featured{border-radius:14px;overflow:hidden;position:relative;cursor:pointer;}
.news-featured img{width:100%;height:clamp(280px,40vh,400px);object-fit:cover;transition:.5s;}
.news-grid{display:grid;grid-template-columns:1fr 1fr;gap:36px;align-items:stretch;}
.news-featured{border-radius:14px;overflow:hidden;position:relative;cursor:pointer;display:flex;}
.news-featured img{width:100%;height:100%;object-fit:cover;transition:.5s;}
.news-featured:hover img{transform:scale(1.03);}
.news-featured .overlay{
position:absolute;bottom:0;left:0;right:0;
@@ -374,8 +374,8 @@ img{max-width:100%;display:block;}
}
.news-item:hover{padding-left:8px;}
.news-item .date-box{flex-shrink:0;width:54px;text-align:center;}
.news-item .date-box .day{font-family:var(--font-en);font-size:24px;font-weight:700;color:var(--orange);line-height:1;}
.news-item .date-box .ym{font-size:12px;color:var(--gray-300);}
.news-item .date-box .day{font-family:var(--font-en);font-size:28px;font-weight:700;color:var(--orange);line-height:1;}
.news-item .date-box .ym{font-size:12px;color:var(--gray-300);white-space:nowrap;margin-top:2px;}
.news-item .text h4{font-size:14px;font-weight:500;color:var(--gray-900);margin-bottom:4px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.news-item .text p{font-size:13px;color:var(--gray-500);display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;}

@@ -537,7 +537,7 @@ img{max-width:100%;display:block;}
.proj-info-card .card-btn{padding:8px 20px;font-size:12px;}
.proj-tabs{bottom:8px;left:8px;right:8px;}
.proj-tab{padding:5px 8px;font-size:11px;}
.news-item .date-box .day{font-size:20px;}
.news-item .date-box .day{font-size:22px;}
.news-item .text h4{font-size:13px;}
.partners-grid{grid-template-columns:repeat(2,1fr);gap:8px;}
.partner-item{padding:12px;border-radius:8px;min-height:70px;}
@@ -552,3 +552,187 @@ img{max-width:100%;display:block;}
.slide-donation{padding-top:56px;}
.slide-news{padding-top:56px;}
}


/* ===== Column Page ===== */
.col-banner{
position:relative;height:280px;
background:center/cover no-repeat var(--orange);
display:flex;align-items:center;justify-content:center;
margin-top:72px;
}
.col-banner-overlay{
position:absolute;inset:0;
background:linear-gradient(180deg,rgba(0,0,0,.15) 0%,rgba(0,0,0,.4) 100%);
}
.col-banner-content{
position:relative;z-index:1;text-align:center;color:#fff;
}
.col-banner-content h1{
font-size:42px;font-weight:700;letter-spacing:2px;margin-bottom:12px;
}
.col-banner-en{
font-family:var(--font-en);font-size:16px;letter-spacing:3px;
text-transform:uppercase;opacity:.9;margin-bottom:16px;
}
.col-banner-desc{
font-size:18px;opacity:.95;letter-spacing:2px;
}

/* Sub Tabs */
.col-tabs-wrap{
background:var(--gray-bg);border-bottom:none;
position:sticky;top:72px;z-index:100;padding:12px 0;
}
.col-tabs{
max-width:1200px;margin:0 auto;display:flex;justify-content:center;
gap:24px;align-items:center;
}
.col-tab{
padding:6px 32px;font-size:16px;color:#606266;
border:none;border-radius:24px;transition:.3s;white-space:nowrap;
font-weight:500;background:transparent;
}
.col-tab:hover{color:#fff;background:rgba(232,117,26,.8);}
.col-tab.active{
color:#fff;background:var(--orange);font-weight:600;
}

/* Breadcrumb */
.col-breadcrumb-wrap{
background:#fff;border-bottom:none;
}
.col-breadcrumb{
max-width:1200px;margin:0 auto;padding:20px 0;
font-size:14px;color:#909399;
}
.col-breadcrumb a{color:#606266;transition:.2s;}
.col-breadcrumb a:hover{color:var(--orange);}
.col-breadcrumb .sep{margin:0 8px;color:#C0C4CC;}
.col-breadcrumb .current{color:var(--orange);}

/* Content Area */
.col-content-wrap{background:#fff;min-height:50vh;}
.col-content{
max-width:1200px;margin:0 auto;padding:40px;
}
.col-empty{
text-align:center;padding:80px 0;color:var(--gray-300);font-size:15px;
}

/* Section Title */
.col-section-title{
font-size:28px;font-weight:700;color:var(--gray-900);
margin-bottom:32px;padding-bottom:16px;
border-bottom:3px solid var(--orange);display:inline-block;
}

/* Page Content */
.col-page-body{line-height:2;font-size:15px;color:var(--gray-700);}
.col-page-body img{max-width:100%;height:auto;border-radius:8px;margin:16px 0;}
.col-page-body p{margin-bottom:16px;}

/* Article List */
.col-article-list{display:flex;flex-direction:column;gap:24px;}
.col-article-item{
display:flex;gap:24px;padding:20px 0;
border-bottom:1px solid var(--gray-100);transition:.2s;
}
.col-article-item:hover{padding-left:8px;}
.col-article-cover{flex-shrink:0;width:240px;height:160px;border-radius:8px;overflow:hidden;}
.col-article-cover img{width:100%;height:100%;object-fit:cover;transition:.3s;}
.col-article-item:hover .col-article-cover img{transform:scale(1.05);}
.col-article-info{flex:1;display:flex;flex-direction:column;justify-content:center;}
.col-article-info h3{font-size:18px;font-weight:600;color:var(--gray-900);margin-bottom:8px;line-height:1.5;}
.col-article-info p{font-size:14px;color:var(--gray-500);line-height:1.8;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}
.col-article-meta{margin-top:auto;padding-top:12px;display:flex;gap:16px;font-size:13px;color:var(--gray-300);}
.col-article-meta .cate{color:var(--orange);}

/* Image Grid (Certificate Style) */
.col-image-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;}
.col-image-item{
background:#fff;border-radius:8px;overflow:hidden;
box-shadow:0 4px 16px rgba(0,0,0,.08);transition:.3s;cursor:pointer;
}
.col-image-item:hover{transform:translateY(-4px);box-shadow:0 12px 32px rgba(232,117,26,.15);}
.col-image-thumb{
width:100%;height:320px;background:var(--gray-bg);
display:flex;align-items:center;justify-content:center;overflow:hidden;
}
.col-image-thumb img{width:100%;height:100%;object-fit:contain;padding:16px;}
.col-image-title{
padding:16px;text-align:center;font-size:14px;color:var(--gray-900);background:#fff;
}

/* Text List */
.col-text-list{display:flex;flex-direction:column;}
.col-text-item{
display:flex;align-items:center;justify-content:space-between;
padding:16px 0;border-bottom:1px solid var(--gray-100);transition:.2s;
}
.col-text-item:hover{padding-left:8px;}
.col-text-title{font-size:15px;color:var(--gray-700);}
.col-text-title a{color:var(--gray-700);transition:.2s;}
.col-text-title a:hover{color:var(--orange);}
.col-text-date{font-size:13px;color:var(--gray-300);flex-shrink:0;margin-left:24px;}

/* Person Grid (Team Member Card Style) */
.col-person-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:32px;}
.col-person-card{
background:#fff;border-radius:12px;overflow:hidden;
box-shadow:0 4px 16px rgba(0,0,0,.08);transition:.3s;
}
.col-person-card:hover{transform:translateY(-4px);box-shadow:0 12px 32px rgba(232,117,26,.15);}
.col-person-photo{
width:100%;height:280px;background:var(--gray-bg);
display:flex;align-items:center;justify-content:center;overflow:hidden;
}
.col-person-photo img{width:100%;height:100%;object-fit:cover;}
.col-person-placeholder{
width:100%;height:100%;display:flex;align-items:center;justify-content:center;
background:var(--orange);color:#fff;font-size:48px;font-weight:700;
}
.col-person-info{padding:24px;}
.col-person-name{font-size:18px;font-weight:600;color:var(--gray-900);margin-bottom:8px;}
.col-person-title{font-size:14px;color:var(--orange);margin-bottom:12px;}
.col-person-bio{font-size:14px;color:#606266;line-height:1.8;}
.col-person-cate{font-size:12px;color:var(--orange);margin-top:4px;}

/* Job List */
.col-job-list{display:flex;flex-direction:column;gap:16px;}
.col-job-item{
padding:24px;border:1px solid var(--gray-100);border-radius:8px;transition:.2s;
}
.col-job-item:hover{border-color:var(--orange);box-shadow:0 4px 16px rgba(232,117,26,.08);}
.col-job-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;}
.col-job-header h3{font-size:18px;font-weight:600;color:var(--gray-900);}
.col-job-loc{font-size:13px;color:var(--gray-500);background:var(--gray-bg);padding:4px 12px;border-radius:4px;}
.col-job-desc{font-size:14px;color:var(--gray-500);line-height:1.8;}

/* Column Page Responsive */
@media(max-width:768px){
.col-banner{height:220px;}
.col-banner-content h1{font-size:28px;}
.col-tabs-wrap{top:56px;}
.col-tabs{overflow-x:auto;justify-content:flex-start;padding:0 16px;-webkit-overflow-scrolling:touch;}
.col-tab{padding:6px 20px;font-size:14px;white-space:nowrap;}
.col-content{padding:24px 20px;}
.col-breadcrumb{padding:16px 20px;}
.col-article-cover{width:140px;height:100px;}
.col-article-info h3{font-size:15px;}
.col-image-grid{grid-template-columns:1fr;gap:16px;}
.col-image-thumb{height:280px;}
.col-person-grid{grid-template-columns:1fr;gap:16px;}
.col-person-photo{height:240px;}
}
@media(max-width:480px){
.col-banner{height:180px;}
.col-banner-content h1{font-size:22px;}
.col-banner-en{font-size:12px;}
.col-tab{padding:5px 16px;font-size:13px;}
.col-content{padding:20px 16px;}
.col-article-item{flex-direction:column;gap:12px;}
.col-article-cover{width:100%;height:180px;}
.col-image-grid{grid-template-columns:1fr;gap:12px;}
.col-person-grid{grid-template-columns:1fr;gap:12px;}
}

Зареждане…
Отказ
Запис