diff --git a/api/app/lib/controllers/label/index.js b/api/app/lib/controllers/label/index.js new file mode 100644 index 0000000..1875cdf --- /dev/null +++ b/api/app/lib/controllers/label/index.js @@ -0,0 +1,71 @@ +'use strict'; + +async function getLabels(ctx, next) { + try { + const models = ctx.fs.dc.models; + + const res = await models.QrcodeLabels.findAll({ + order: [['id', 'DESC']], + }) + + ctx.status = 200; + ctx.body = res + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + "message": "获取标签失败" + } + } +} + +async function createLabels(ctx, next) { + const models = ctx.fs.dc.models; + try { + let rslt = ctx.request.body; + await models.Department.create(Object.assign({}, rslt, { read: 1 })) + ctx.status = 204; + ctx.body = { message: '新建部门成功' } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '新建部门失败' } + } +} + +async function delLabels(ctx, next) { + let errMsg = "删除部门失败"; + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + let list = await models.Department.findAll({}); + let deptIds = list.map(l => l.id); + const allUsers = await models.User.findAll({ + where: { + departmentId: { $in: deptIds }, + delete: false + } + }) + const childrenDept = await models.Department.findAll({ where: { dependence: id } }) + const childrenUser = allUsers.filter(au => au.departmentId == id); + if (childrenUser.length || childrenDept.length) { + errMsg = '请先删除部门下的用户或子部门'; + throw errMsg; + } + await models.Department.destroy({ where: { id: id } }); + await models.Department.destroy({ where: { dependence: id } }); + ctx.status = 204; + ctx.body = { message: '删除部门成功' } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: error } + } +} + + +module.exports = { + getLabels, + createLabels, + delLabels, +} \ No newline at end of file diff --git a/api/app/lib/controllers/publicityInfo/index.js b/api/app/lib/controllers/publicityInfo/index.js new file mode 100644 index 0000000..0d5ade5 --- /dev/null +++ b/api/app/lib/controllers/publicityInfo/index.js @@ -0,0 +1,173 @@ +'use strict'; +const Hex = require('crypto-js/enc-hex'); +const MD5 = require('crypto-js/md5'); + +async function getPublicityInfo(ctx, next) { + try { + const models = ctx.fs.dc.models; + const { limit, page, name, label } = ctx.query + + let findOption = { + where: {}, + order: [['time', 'DESC']], + include: [ + { + model: models.Qrcode, + }, + { + model: models.QrcodeLabels, + attributes: ["name"], + }, + { + model: models.QrcodeFiles, + }, + ], + distinct: true + } + if (limit) { + findOption.limit = Number(limit) + } + if (page && limit) { + findOption.offset = page * limit + } + // if (startTime && endTime) { + // findOption.where.time = { $between: [startTime, endTime] }; + // } + if (name) { + findOption.where.name = { $like: `%${name}%` } + } + + const res = await models.PublicityInfo.findAndCountAll(findOption) + + ctx.status = 200; + ctx.body = res + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + "message": "获取宣传信息失败" + } + } +} + +async function createPublicityInfo(ctx, next) { + const models = ctx.fs.dc.models; + const transaction = await ctx.fs.dc.orm.transaction(); + try { + const body = ctx.request.body; + const { type, name, labels, qrcode, files } = body; + + const dbQrcode = await models.Qrcode.create({ + name, type, url: qrcode.url, key: qrcode.key, logo: qrcode.logo, + }, { transaction }); + + const dbPublicityInfo = await models.PublicityInfo.create({ + ...body, + qrcodeId: dbQrcode.id, + time: new Date().getTime(), + }, { transaction }); + + if (labels && labels.length) { + let labelIds = [] + let createLabels = []; + for (const l of labels) { + if (typeof l === 'string') { + createLabels.push({ name: l }); + } else { + labelIds.push(l); + } + } + if (createLabels.length) { + const dbLabels = await models.QrcodeLabels.bulkCreate(createLabels, { returning: true, transaction }); + labelIds = labelIds.concat(dbLabels.map(l => l.id)); + } + if (labelIds.length) { + await models.QrcodeLabelsQrcode.bulkCreate(labelIds.map(l => ({ + labelId: l, qrcodeId: dbQrcode.id, publicityInfoId: dbPublicityInfo.id + })), { transaction }); + } + } + + if (type !== '链接') { + if (!files || !files.length) throw new Error('缺少请求参数: files'); + await models.QrcodeFiles.bulkCreate(files.map(f => ({ + fileName: f.customFileName ? f.customFileName : f.name.split('.').slice(0, -1).join('.'), + fileSize: f.size, + fileUrl: f.storageUrl, + previewImgUrl: f.previewImgUrl, + qrcodeId: dbQrcode.id, + publicityInfoId: dbPublicityInfo.id, + })), { transaction }); + } + + ctx.status = 204; + ctx.body = { message: '新建宣传信息成功' }; + await transaction.commit(); + } catch (error) { + await transaction.rollback(); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '新建宣传信息失败' }; + } +} + +async function updatePublicityInfo(ctx, next) { + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + const body = ctx.request.body; + await models.Department.update( + body, + { where: { id: id } } + ) + ctx.status = 204; + ctx.body = { message: '修改部门成功' } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '修改部门失败' } + } +} + +async function delPublicityInfo(ctx, next) { + const transaction = await ctx.fs.dc.orm.transaction(); + let errMsg = "删除宣传信息失败"; + try { + const models = ctx.fs.dc.models; + const { id } = ctx.params; + const { qrcodeId } = ctx.query; + + await models.QrcodeLabelsQrcode.destroy({ + where: { publicityInfoId: id }, + transaction + }); + await models.QrcodeFiles.destroy({ + where: { publicityInfoId: id }, + transaction + }); + await models.PublicityInfo.destroy({ + where: { id: id }, + transaction: transaction, + }); + await models.Qrcode.destroy({ + where: { id: qrcodeId }, + transaction + }); + + ctx.status = 204; + ctx.body = { message: '删除宣传信息成功' } + transaction.commit(); + } catch (error) { + transaction.rollback(); + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: error } + } +} + +module.exports = { + getPublicityInfo, + createPublicityInfo, + updatePublicityInfo, + delPublicityInfo, +} \ No newline at end of file diff --git a/api/app/lib/controllers/qrcode/index.js b/api/app/lib/controllers/qrcode/index.js new file mode 100644 index 0000000..eed0b95 --- /dev/null +++ b/api/app/lib/controllers/qrcode/index.js @@ -0,0 +1,56 @@ +'use strict'; + +async function getQrcode(ctx, next) { + try { + const models = ctx.fs.dc.models; + const { limit, page, name, key } = ctx.query + + let options = { + where: {}, + order: [['id', 'DESC']], + include: [] + } + if (name) { + options.where.name = { $like: `%${name}%` } + } + if (key) { + options.where.key = key + options.include.push({ + model: models.QrcodeFiles, + }) + options.distinct = true + } + + const res = await models.Qrcode.findAndCountAll(options) + + ctx.status = 200; + ctx.body = res + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + "message": "获取二维码失败" + } + } +} + +async function createQrcode(ctx, next) { + const models = ctx.fs.dc.models; + try { + let rslt = ctx.request.body; + + const qrcode = await models.Qrcode.create(rslt) + + ctx.status = 204; + ctx.body = { message: '新增二维码成果', id: qrcode.id } + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { message: '新增二维码失败' } + } +} + +module.exports = { + getQrcode, + createQrcode, +} \ No newline at end of file diff --git a/api/app/lib/index.js b/api/app/lib/index.js index 4ff7bca..56b24e8 100644 --- a/api/app/lib/index.js +++ b/api/app/lib/index.js @@ -53,7 +53,10 @@ module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Seq require(`./models/${filename}`)(dc) }); - const { Department, User, UserResource, Resource, } = dc.models; + const { + Department, User, UserResource, Resource, Qrcode, QrcodeFiles, QrcodeLabels, + QrcodeLabelsQrcode, PublicityInfo, + } = dc.models; UserResource.belongsTo(User, { foreignKey: 'userId', targetKey: 'id' }); User.hasMany(UserResource, { foreignKey: 'userId', sourceKey: 'id' }); @@ -65,4 +68,15 @@ module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Seq User.belongsTo(Department, { foreignKey: 'departmentId', targetKey: 'id' }); Department.hasMany(User, { foreignKey: 'departmentId', sourceKey: 'id' }); + Qrcode.belongsToMany(QrcodeLabels, { through: QrcodeLabelsQrcode, foreignKey: 'qrcodeId', otherKey: 'labelId' }); + QrcodeLabels.belongsToMany(Qrcode, { through: QrcodeLabelsQrcode, foreignKey: 'labelId', otherKey: 'qrcodeId' }); + PublicityInfo.belongsToMany(QrcodeLabels, { through: QrcodeLabelsQrcode, foreignKey: 'publicityInfoId', otherKey: 'labelId' }); + QrcodeLabels.belongsToMany(PublicityInfo, { through: QrcodeLabelsQrcode, foreignKey: 'labelId', otherKey: 'publicityInfoId' }); + + QrcodeFiles.belongsTo(Qrcode, { foreignKey: 'qrcodeId', targetKey: 'id' }); + Qrcode.hasMany(QrcodeFiles, { foreignKey: 'qrcodeId', sourceKey: 'id' }); + QrcodeFiles.belongsTo(PublicityInfo, { foreignKey: 'publicityInfoId', targetKey: 'id' }); + PublicityInfo.hasMany(QrcodeFiles, { foreignKey: 'publicityInfoId', sourceKey: 'id' }); + PublicityInfo.belongsTo(Qrcode, { foreignKey: 'qrcodeId', targetKey: 'id' }); + Qrcode.hasMany(PublicityInfo, { foreignKey: 'qrcodeId', sourceKey: 'id' }); }; diff --git a/api/app/lib/middlewares/authenticator.js b/api/app/lib/middlewares/authenticator.js index 447690d..bce3ec1 100644 --- a/api/app/lib/middlewares/authenticator.js +++ b/api/app/lib/middlewares/authenticator.js @@ -13,13 +13,13 @@ class ExcludesUrls { this.reload(opts); } - sanitizePath (path) { + sanitizePath(path) { if (!path) return '/'; const p = '/' + path.replace(/^\/+/i, '').replace(/\/+$/, '').replace(/\/{2,}/, '/'); return p; } - reload (opts) { + reload(opts) { // load all url if (!this.allUrls) { this.allUrls = opts; @@ -37,7 +37,7 @@ class ExcludesUrls { } } - isExcluded (path, method) { + isExcluded(path, method) { return this.allUrls.some(function (url) { return !url.auth && url.pregexp.test(path) @@ -59,6 +59,7 @@ let isPathExcluded = function (opts, path, method) { let excludeOpts = opts.exclude || []; excludeOpts.push({ p: '/login', o: 'POST' }); excludeOpts.push({ p: '/logout', o: 'PUT' }); + excludeOpts.push({ p: '/qrcode', o: 'GET' }); excludes = new ExcludesUrls(excludeOpts); } let excluded = excludeAll || excludes.isExcluded(path, method); @@ -110,8 +111,8 @@ let isResourceAvailable = function (resources, options) { return !authCode || (resources || []).some(code => code === authCode); }; -function factory (app, opts) { - return async function auth (ctx, next) { +function factory(app, opts) { + return async function auth(ctx, next) { const { path, method, header, query } = ctx; ctx.fs.logger.log('[AUTH] start', path, method); ctx.fs.api = ctx.fs.api || {}; diff --git a/api/app/lib/models/publicity_info.js b/api/app/lib/models/publicity_info.js new file mode 100644 index 0000000..7746009 --- /dev/null +++ b/api/app/lib/models/publicity_info.js @@ -0,0 +1,74 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const PublicityInfo = sequelize.define("publicityInfo", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "宣传标题", + primaryKey: false, + field: "name", + autoIncrement: false, + }, + type: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "类型(图片|文件|链接|视频)", + primaryKey: false, + field: "type", + autoIncrement: false, + }, + time: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: null, + comment: "创建/更新时间", + primaryKey: false, + field: "time", + autoIncrement: false, + }, + qrcodeId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: "关联二维码ID", + primaryKey: false, + field: "qrcode_id", + autoIncrement: false, + references: { + model: "qrcode", + key: "id" + } + }, + link: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "type为链接时的链接地址", + primaryKey: false, + field: "link", + autoIncrement: false, + }, + }, { + tableName: "publicity_info", + comment: "", + indexes: [] + }); + + dc.models.PublicityInfo = PublicityInfo; + return PublicityInfo; +}; \ No newline at end of file diff --git a/api/app/lib/models/qrcode.js b/api/app/lib/models/qrcode.js new file mode 100644 index 0000000..497386a --- /dev/null +++ b/api/app/lib/models/qrcode.js @@ -0,0 +1,70 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const Qrcode = sequelize.define("qrcode", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + url: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "二维码图片存储链接", + primaryKey: false, + field: "url", + autoIncrement: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "二维码名称", + primaryKey: false, + field: "name", + autoIncrement: false, + }, + type: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "类型(图片|文件|链接|视频)", + primaryKey: false, + field: "type", + autoIncrement: false, + }, + key: { + type: DataTypes.UUID, + allowNull: false, + defaultValue: null, + comment: "二维码唯一标识", + primaryKey: false, + field: "key", + autoIncrement: false, + }, + logo: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "二维码中间的照片", + primaryKey: false, + field: "logo", + autoIncrement: false, + }, + }, { + tableName: "qrcode", + comment: "", + indexes: [] + }); + + dc.models.Qrcode = Qrcode; + return Qrcode; +}; \ No newline at end of file diff --git a/api/app/lib/models/qrcode_files.js b/api/app/lib/models/qrcode_files.js new file mode 100644 index 0000000..c6315b8 --- /dev/null +++ b/api/app/lib/models/qrcode_files.js @@ -0,0 +1,87 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const QrcodeFiles = sequelize.define("qrcodeFiles", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + fileName: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "文件名", + primaryKey: false, + field: "file_name", + autoIncrement: false, + }, + fileSize: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: "文件大小(byte)", + primaryKey: false, + field: "file_size", + autoIncrement: false, + }, + fileUrl: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "文件存储链接", + primaryKey: false, + field: "file_url", + autoIncrement: false, + }, + previewImgUrl: { + type: DataTypes.STRING, + allowNull: true, + defaultValue: null, + comment: "文件预览图链接", + primaryKey: false, + field: "preview_img_url", + autoIncrement: false, + }, + qrcodeId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: "关联二维码ID", + primaryKey: false, + field: "qrcode_id", + autoIncrement: false, + references: { + model: "qrcode", + key: "id" + } + }, + publicityInfoId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: "关联宣传信息ID", + primaryKey: false, + field: "publicity_info_id", + autoIncrement: false, + references: { + model: "publicity_info", + key: "id" + } + }, + }, { + tableName: "qrcode_files", + comment: "", + indexes: [] + }); + + dc.models.QrcodeFiles = QrcodeFiles; + return QrcodeFiles; +}; \ No newline at end of file diff --git a/api/app/lib/models/qrcode_labels.js b/api/app/lib/models/qrcode_labels.js new file mode 100644 index 0000000..ac99322 --- /dev/null +++ b/api/app/lib/models/qrcode_labels.js @@ -0,0 +1,34 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const QrcodeLabels = sequelize.define("qrcodeLabels", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: null, + comment: "标签名", + primaryKey: false, + field: "name", + autoIncrement: false, + }, + }, { + tableName: "qrcode_labels", + comment: "", + indexes: [] + }); + + dc.models.QrcodeLabels = QrcodeLabels; + return QrcodeLabels; +}; \ No newline at end of file diff --git a/api/app/lib/models/qrcode_labels_qrcode.js b/api/app/lib/models/qrcode_labels_qrcode.js new file mode 100644 index 0000000..d7b694a --- /dev/null +++ b/api/app/lib/models/qrcode_labels_qrcode.js @@ -0,0 +1,64 @@ +/* eslint-disable*/ +'use strict'; + +module.exports = dc => { + const DataTypes = dc.ORM; + const sequelize = dc.orm; + const QrcodeLabelsQrcode = sequelize.define("qrcode_labels_qrcode", { + id: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: true, + field: "id", + autoIncrement: true, + }, + labelId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "label_id", + autoIncrement: false, + references: { + model: "qrcode_labels", + key: "id" + } + }, + qrcodeId: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + comment: null, + primaryKey: false, + field: "qrcode_id", + autoIncrement: false, + references: { + model: "qrcode", + key: "id" + } + }, + publicityInfoId: { + type: DataTypes.INTEGER, + allowNull: true, + defaultValue: null, + comment: null, + primaryKey: false, + field: "publicity_info_id", + autoIncrement: false, + references: { + model: "publicity_info", + key: "id" + } + }, + }, { + tableName: "qrcode_labels_qrcode", + comment: "", + indexes: [] + }); + + dc.models.QrcodeLabelsQrcode = QrcodeLabelsQrcode; + return QrcodeLabelsQrcode; +}; \ No newline at end of file diff --git a/api/app/lib/routes/label/index.js b/api/app/lib/routes/label/index.js new file mode 100644 index 0000000..4fbac74 --- /dev/null +++ b/api/app/lib/routes/label/index.js @@ -0,0 +1,15 @@ +'use strict'; + +const label = require('../../controllers/label/index'); + +module.exports = function (app, router, opts) { + + app.fs.api.logAttr['GET/label'] = { content: '获取标签', visible: false }; + router.get('/label', label.getLabels); + + app.fs.api.logAttr['POST/label/add'] = { content: '新增标签', visible: true }; + router.post('/label/add', label.createLabels); + + app.fs.api.logAttr['DELETE/label/:id'] = { content: '删除标签', visible: true }; + router.del('/label/:id', label.delLabels); +}; \ No newline at end of file diff --git a/api/app/lib/routes/publicityInfo/index.js b/api/app/lib/routes/publicityInfo/index.js new file mode 100644 index 0000000..5a95905 --- /dev/null +++ b/api/app/lib/routes/publicityInfo/index.js @@ -0,0 +1,18 @@ +'use strict'; + +const publicityInfo = require('../../controllers/publicityInfo/index'); + +module.exports = function (app, router, opts) { + + app.fs.api.logAttr['GET/publicityInfo'] = { content: '获取宣传信息', visible: false }; + router.get('/publicityInfo', publicityInfo.getPublicityInfo); + + app.fs.api.logAttr['POST/publicityInfo/add'] = { content: '新增宣传信息', visible: true }; + router.post('/publicityInfo/add', publicityInfo.createPublicityInfo); + + app.fs.api.logAttr['PUT/publicityInfo/:id/modify'] = { content: '修改宣传信息', visible: true }; + router.put('/publicityInfo/:id/modify', publicityInfo.updatePublicityInfo); + + app.fs.api.logAttr['DELETE/publicityInfo/:id/del'] = { content: '删除宣传信息', visible: true }; + router.del('/publicityInfo/:id/del', publicityInfo.delPublicityInfo); +}; \ No newline at end of file diff --git a/api/app/lib/routes/qrcode/index.js b/api/app/lib/routes/qrcode/index.js new file mode 100644 index 0000000..00856c1 --- /dev/null +++ b/api/app/lib/routes/qrcode/index.js @@ -0,0 +1,13 @@ +'use strict'; + +const qrcode = require('../../controllers/qrcode/index'); + +module.exports = function (app, router, opts) { + + app.fs.api.logAttr['GET/qrcode'] = { content: '获取二维码', visible: false }; + router.get('/qrcode', qrcode.getQrcode); + + app.fs.api.logAttr['POST/qrcode/add'] = { content: '新增二维码', visible: true }; + router.post('/qrcode/add', qrcode.createQrcode); + +}; \ No newline at end of file diff --git a/script/1.0.0/schema/2.init_qrcode.sql b/script/1.0.0/schema/2.init_qrcode.sql index 4fdf83c..33c582a 100644 --- a/script/1.0.0/schema/2.init_qrcode.sql +++ b/script/1.0.0/schema/2.init_qrcode.sql @@ -4,11 +4,15 @@ CREATE TABLE "public"."qrcode" ( "url" varchar(255) NOT NULL, "name" varchar(128) NOT NULL, "type" varchar(32) NOT NULL, + "key" uuid NOT NULL, + "logo" varchar(255), PRIMARY KEY ("id") ); -COMMENT ON COLUMN "public"."qrcode"."name" IS '二维码名称'; -COMMENT ON COLUMN "public"."qrcode"."type" IS '类型(文件|链接)'; COMMENT ON COLUMN "public"."qrcode"."url" IS '二维码图片存储链接'; +COMMENT ON COLUMN "public"."qrcode"."name" IS '二维码名称'; +COMMENT ON COLUMN "public"."qrcode"."type" IS '类型(图片|文件|链接|视频)'; +COMMENT ON COLUMN "public"."qrcode"."key" IS '二维码唯一标识'; +COMMENT ON COLUMN "public"."qrcode"."logo" IS '二维码中间的照片'; DROP TABLE IF EXISTS "public"."publicity_info"; @@ -17,27 +21,56 @@ CREATE TABLE "public"."publicity_info" ( "name" varchar(128) NOT NULL, "type" varchar(32) NOT NULL, "time" timestamptz NOT NULL, - "qrcode_id" int4 NOT NULL, + "link" varchar(255), + "qrcode_id" int4, PRIMARY KEY ("id"), CONSTRAINT "publicity_info_qrcode_id_fk" FOREIGN KEY ("qrcode_id") REFERENCES "public"."qrcode" ("id") ); COMMENT ON COLUMN "public"."publicity_info"."name" IS '宣传标题'; -COMMENT ON COLUMN "public"."publicity_info"."type" IS '类型'; +COMMENT ON COLUMN "public"."publicity_info"."type" IS '类型(图片|文件|链接|视频)'; COMMENT ON COLUMN "public"."publicity_info"."time" IS '创建/更新时间'; +COMMENT ON COLUMN "public"."publicity_info"."link" IS 'type为链接时的链接地址'; COMMENT ON COLUMN "public"."publicity_info"."qrcode_id" IS '关联二维码ID'; DROP TABLE IF EXISTS "public"."qrcode_files"; CREATE TABLE "public"."qrcode_files" ( "id" serial, - "qrcode_id" int4 NOT NULL, "file_name" varchar(128) NOT NULL, "file_size" int4 NOT NULL, "file_url" varchar(255) NOT NULL, + "preview_img_url" varchar(255), + "qrcode_id" int4, + "publicity_info_id" int4, PRIMARY KEY ("id"), - CONSTRAINT "qrcode_files_qrcode_id_fk" FOREIGN KEY ("qrcode_id") REFERENCES "public"."qrcode" ("id") + CONSTRAINT "qrcode_files_qrcode_id_fk" FOREIGN KEY ("qrcode_id") REFERENCES "public"."qrcode" ("id"), + CONSTRAINT "qrcode_files_publicity_info_id_fk" FOREIGN KEY ("publicity_info_id") REFERENCES "public"."publicity_info" ("id") ); -COMMENT ON COLUMN "public"."qrcode_files"."qrcode_id" IS '关联二维码ID'; COMMENT ON COLUMN "public"."qrcode_files"."file_name" IS '文件名'; COMMENT ON COLUMN "public"."qrcode_files"."file_size" IS '文件大小(byte)'; COMMENT ON COLUMN "public"."qrcode_files"."file_url" IS '文件存储链接'; +COMMENT ON COLUMN "public"."qrcode_files"."preview_img_url" IS '文件预览图链接'; +COMMENT ON COLUMN "public"."qrcode_files"."qrcode_id" IS '关联二维码ID'; +COMMENT ON COLUMN "public"."qrcode_files"."publicity_info_id" IS '关联宣传信息ID'; + + +DROP TABLE IF EXISTS "public"."qrcode_labels"; +CREATE TABLE "public"."qrcode_labels" ( + "id" serial, + "name" varchar(128) NOT NULL, + PRIMARY KEY ("id") +); +COMMENT ON COLUMN "public"."qrcode_labels"."name" IS '标签名'; + + +DROP TABLE IF EXISTS "public"."qrcode_labels_qrcode"; +CREATE TABLE "public"."qrcode_labels_qrcode" ( + "id" serial, + "label_id" int4 NOT NULL, + "qrcode_id" int4 NOT NULL, + "publicity_info_id" int4, + PRIMARY KEY ("id"), + CONSTRAINT "qrcode_labels_label_id_fk" FOREIGN KEY ("label_id") REFERENCES "public"."qrcode_labels" ("id"), + CONSTRAINT "qrcode_labels_qrcode_id_fk" FOREIGN KEY ("qrcode_id") REFERENCES "public"."qrcode" ("id"), + CONSTRAINT "qrcode_labels_publicity_info_id_fk" FOREIGN KEY ("publicity_info_id") REFERENCES "public"."publicity_info" ("id") +); \ No newline at end of file diff --git a/web/client/assets/images/document.svg b/web/client/assets/images/document.svg new file mode 100644 index 0000000..9efb51f --- /dev/null +++ b/web/client/assets/images/document.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/client/assets/images/logo.png b/web/client/assets/images/logo.png index 87a6aa3..6a79c8b 100644 Binary files a/web/client/assets/images/logo.png and b/web/client/assets/images/logo.png differ diff --git a/web/client/assets/images/logo.svg b/web/client/assets/images/logo.svg deleted file mode 100644 index cc6e46d..0000000 --- a/web/client/assets/images/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/web/client/assets/images/logo_white.png b/web/client/assets/images/logo_white.png new file mode 100644 index 0000000..3016c79 Binary files /dev/null and b/web/client/assets/images/logo_white.png differ diff --git a/web/client/index.ejs b/web/client/index.ejs index 5a73b8d..0431b54 100644 --- a/web/client/index.ejs +++ b/web/client/index.ejs @@ -3,13 +3,14 @@ + - --> diff --git a/web/client/index.html b/web/client/index.html index 95afad8..276534f 100644 --- a/web/client/index.html +++ b/web/client/index.html @@ -3,14 +3,15 @@ + - + - --> diff --git a/web/client/src/components/QrcodeFilesUpload/fileInfoForm.js b/web/client/src/components/QrcodeFilesUpload/fileInfoForm.js new file mode 100644 index 0000000..519cfdd --- /dev/null +++ b/web/client/src/components/QrcodeFilesUpload/fileInfoForm.js @@ -0,0 +1,52 @@ +import React, { useRef } from 'react' +import { Spin, Upload, message, Modal, Card, Button, Input, Form } from 'antd' +import Uploads from '../Uploads' +export default function FileInfoForm({ }) { + const [form] = Form.useForm(); + + const handleOk = () => { + form.validateFields() + .then((values) => { + console.log(values, 'valuesvalues') + }) + .catch((info) => { + console.log('Validate Failed:', info); + }); + } + + return ( +
+ + + + + { + return { + storageUrl: s + } + })} + /> + +
+ ) +} diff --git a/web/client/src/components/QrcodeFilesUpload/index.js b/web/client/src/components/QrcodeFilesUpload/index.js new file mode 100644 index 0000000..c05dfc6 --- /dev/null +++ b/web/client/src/components/QrcodeFilesUpload/index.js @@ -0,0 +1,413 @@ +'use strict'; + +import React, { Component, useRef } from 'react'; +import { connect } from 'react-redux'; +import { Spin, Upload, message, Modal, Card, Button, Input, Form } from 'antd'; +import Uploads from '../Uploads'; +import moment from 'moment'; +import { PlusOutlined, UploadOutlined, CloseOutlined } from '@ant-design/icons'; +import './index.less' + +class QrcodeFilesUpload extends Component { + constructor(props) { + super(props); + this.ApiRoot = localStorage.getItem('tyApiRoot') + this.qnDomain = localStorage.getItem('qnDomain'); + this.aliAdmin = localStorage.getItem('aliAdmin'); + this.state = { + fileUploading: false, + fileList: [], + curPreviewPic: '', + curPreviewVideo: '', + delPicIng: false, + removeFilesList: [] + }; + } + + dealName = (uploaded) => { + let realName = uploaded?.split('/')[2] + return realName + } + + setFileList = (nextEditData, isQiniu, isAli) => { + let defaultFileList = []; + defaultFileList = nextEditData.map((u, index) => { + let fileUrl = + isQiniu ? `/_file-server/${u.storageUrl}` + : isAli ? `/_file-ali-server/${u.storageUrl}` + : `${this.ApiRoot}/${u.storageUrl}`; + + return { + uid: -index - 1, + name: this.dealName(u.storageUrl), + status: 'done', + storageUrl: u.storageUrl, + url: fileUrl, + size: u.size || -1, + customFileName: u.customFileName, + previewImgUrl: u.previewImgUrl, + }; + }); + this.setState({ + fileList: defaultFileList + }); + }; + + componentDidMount() { + const { value, defaultValue, isQiniu, isAli } = this.props; + if (defaultValue) { + this.setFileList(defaultValue, isQiniu, isAli) + } + } + + UNSAFE_componentWillReceiveProps(np) { + const { dispatch, value: thisEditData, onChange } = this.props; + const { value: nextEditData, isQiniu, isAli } = np; + if (nextEditData && nextEditData.length) { + if (!thisEditData || !this.state.fileList.length) { + this.setFileList(nextEditData, isQiniu, isAli); + } else if (nextEditData.length != thisEditData.length) { + this.setFileList(nextEditData, isQiniu, isAli); + } else { + let repeat = true; + for (let i = 0; i < thisEditData.length; i++) { + if (thisEditData[i] != nextEditData[i]) { + repeat = false; + break; + } + } + if (!repeat) { + this.setFileList(nextEditData, isQiniu, isAli); + } + } + } + } + + render() { + const UploadPath = { + project: ['txt', 'dwg', 'doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf', 'pptx', 'png', 'jpg', 'svg', 'jpeg', 'rar', 'zip', 'jpeg', 'mp4'], + report: ['doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf'], + data: ['txt', 'xls', 'xlsx', 'csv'], + image: ['png', 'jpg', 'svg', 'jpeg'], + three: ['js'], + video: ['mp4'] + }; + /** + * uploadType 【string】 主要区别文件上传路径 以及类型 以 web/routes/attachment/index.js 中 UploadPath 的 key 值为准;默认 project; + * disabled 【boolean】 上传是否可用 + * maxFilesNum 【number】 最大上传数量 + * fileTypes 【array[string]】 可允许上传的文件类型; + * maxFileSize 【number】 单个文件最大大小 M + * listType 【antd】 upload 组件的属性 + * onChange 【function】 文件数量变化时候回调 返回文件 + * value 【array[obj]】 编辑数据 [{url:'xxx', [size:999]}] + * onStateChange 【function】 文件状态改变回调函数 上传中 return { uploading:true/false } + */ + const { + uploadType, + disabled, + maxFilesNum, + fileTypes, + maxFileSize, + listType, + onChange = () => { }, + value, + showUploadList, + onStateChange, + isQiniu, + isAli, + } = this.props; + const { fileList, curPreviewPic, curPreviewVideo, delPicIng, removeFilesList } = this.state; + const that = this; + let uploadType_ = uploadType || 'project'; + let maxFilesNum_ = maxFilesNum || 1; + let defaultFileTypes = fileTypes || UploadPath[uploadType_]; + // debugger + const uploadProps = { + name: 'checkFile_', + multiple: false, + showUploadList: showUploadList || true, + action: + isQiniu ? `/_upload/attachments/${uploadType_}` + : isAli ? `/_upload/attachments/ali/${uploadType_}` + : `${this.ApiRoot}/attachments/${uploadType_}`, + listType: listType || 'text', + disabled: disabled, + beforeUpload: (file) => { + if (fileList.length >= maxFilesNum_) { + message.warning(`最多选择${maxFilesNum_}个文件上传`); + return false; + } + if (file.name.length > 60) { + message.warning(`文件名过长(大于60字符),请修改后上传`); + return false; + } + const extNames = file.name?.split('.'); + // var reg = /^[\.\s\u4e00-\u9fa5a-zA-Z0-9_-]{0,}$/; + // if (!reg.exec(file.name)) { + // message.warning(`文件名包含除字母、汉字、数字、中划线、下划线之外的字符,请修改后上传`); + // return false; + // } + let isDAE = false; + if (extNames.length > 0) { + let fileType = extNames[extNames.length - 1].toLowerCase(); + isDAE = defaultFileTypes.some((f) => f == fileType); + } + if (!isDAE) { + message.error(`只能上传 ${defaultFileTypes.join()} 格式的文件!`); + return false; + } + const isLt = file.size / 1024 / 1024 < (maxFileSize || 3); + if (!isLt) { + message.error(`文件必须小于${maxFileSize || 3}MB!`); + return false; + } + this.setState({ + fileUploading: true + }); + if (onStateChange) { + onStateChange({ uploading: true }); + } + }, + onChange(info) { + const status = info.file.status; + if (status === 'uploading') { + that.setState({ + fileList: info.fileList + }); + } + if (status === 'done') { + let { uploaded, url } = info.file.response; + let size = info.file.size; + let nextFileList = fileList; + nextFileList[nextFileList.length - 1] = { + uid: -moment().unix(), + name: that.dealName(uploaded), + status: 'done', + storageUrl: uploaded, + url: + isQiniu ? '/_file-server/' + uploaded : + isAli ? `/_file-ali-server/${uploaded}` : + url, + size: size + }; + onChange(nextFileList); + that.setState({ + fileUploading: false, + fileList: nextFileList + }); + if (onStateChange) { + onStateChange({ uploading: false }); + } + } else if (status === 'error') { + that.setState({ + fileUploading: false + }); + message.error(`${info.file.name} 上传失败,请重试`); + if (onStateChange) { + onStateChange({ uploading: false }); + } + } + }, + onRemove(file) { + let nextFileList = []; + fileList.map((f, i) => { + if (f.uid != file.uid) { + nextFileList.push(f); + } + }); + let nextRemoveFiles = removeFilesList.concat([file.storageUrl]); + if (curPreviewPic == file.url) { + that.setState({ + curPreviewPic: '' + }); + } + if (curPreviewVideo == file.url) { + that.setState({ + curPreviewVideo: '' + }); + } + onChange(nextFileList); + that.setState({ + fileList: nextFileList, + removeFilesList: nextRemoveFiles + }); + }, + onPreview(file) { + let filePostfix = file.url?.split('.').pop(); + filePostfix = filePostfix.toLowerCase(); + if (UploadPath.image.some((img) => img == filePostfix)) { + that.setState({ + curPreviewPic: file.url + }); + } else if (UploadPath.video.some((img) => img == filePostfix)) { + that.setState({ + curPreviewVideo: file.url + }); + } else { + //message.warn('仅支持图片预览'); + preview(file.storageUrl) + } + } + }; + + const preview = (url) => { + let link = isQiniu ? encodeURI(`${this.qnDomain}/${url}`) : + isAli ? encodeURI(`${this.aliAdmin}/${url}`) : '' + if (link) + if (url.indexOf("pdf") !== -1 || url.indexOf("csv") !== -1) { + window.open(link) + } else { + window.open(`https://view.officeapps.live.com/op/view.aspx?src=${link}`) + } + } + + let fileList_ = fileList + //下载文件 + const handleDownload = (file) => { + saveAs(file) + }; + const saveAs = (file) => { + const link = document.createElement('a'); + link.href = file.url; + link.download = file.name; + link.style.display = 'none'; + link.click(); + } + + //自定义下载 + + return ( +
+ + { + const fileName = file.name?.split('.').slice(0, -1).join('.') + const extension = file.name?.split('.').pop() + return <> + {originNode} +
+ + { + const fileInfo = { + ...file, + customFileName: value, + }; + const nextFileList = currFileList.toSpliced( + currFileList.findIndex(item => item.storageUrl === file.storageUrl), + 1, + fileInfo, + ); + onChange(nextFileList); + this.setState({ fileList: nextFileList }) + }} + placeholder="不填写默认为上传的文件名" + style={{ width: '220px' }} + /> + + {!UploadPath.image.includes(extension) && + { + const fileInfo = { + ...file, + previewImgUrl: previewFile[0]?.storageUrl, + }; + const nextFileList = currFileList.toSpliced( + currFileList.findIndex(item => item.storageUrl === file.storageUrl), + 1, + fileInfo, + ); + onChange(nextFileList); + this.setState({ fileList: nextFileList }) + }} + defaultValue={([]).map(s => { + return { + storageUrl: s + } + })} + /> + } +
+ + }} + > + { + disabled ? ('') : + listType == 'picture-card' ? + ( + fileList.length >= maxFilesNum_ ? null : ( +
+ +
添加附件
+
+ ) + ) : ( + + ) + } +
+ { + curPreviewPic ? ( + +
+ 图片预览 + { this.setState({ curPreviewPic: '' }) }}> + + +
+ +
+ ) : '' + } + { + curPreviewVideo ? ( +
+ 视频预览 + { this.setState({ curPreviewVideo: '' }) }}> + + +
+ +
) : '' + } +
+
+ ); + } +} + +function mapStateToProps(state) { + const { auth } = state + return { + user: auth.user + }; +} + +export default connect(mapStateToProps)(QrcodeFilesUpload); \ No newline at end of file diff --git a/web/client/src/components/QrcodeFilesUpload/index.less b/web/client/src/components/QrcodeFilesUpload/index.less new file mode 100644 index 0000000..e69de29 diff --git a/web/client/src/layout/components/header/index.js b/web/client/src/layout/components/header/index.js index 2e78564..8425cfe 100644 --- a/web/client/src/layout/components/header/index.js +++ b/web/client/src/layout/components/header/index.js @@ -29,7 +29,7 @@ const Header = props => { return (
- +
飞尚码尚来二维码管理系统
FREESUN QRCODE MANAGEMENT SYSTEM
diff --git a/web/client/src/sections/organization/actions/authority.js b/web/client/src/sections/organization/actions/authority.js index d5f0719..40f3d2c 100644 --- a/web/client/src/sections/organization/actions/authority.js +++ b/web/client/src/sections/organization/actions/authority.js @@ -3,16 +3,16 @@ import { basicAction } from '@peace/utils' import { ApiTable } from '$utils' -export function getAuthority(orgId) { - return dispatch => basicAction({ - type: 'get', - dispatch: dispatch, - actionType: 'GET_MEMBERS', - url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, - msg: { error: '获取用户列表失败' }, - reducer: { name: 'members' } - }); -} +// export function getAuthority(orgId) { +// return dispatch => basicAction({ +// type: 'get', +// dispatch: dispatch, +// actionType: 'GET_MEMBERS', +// url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, +// msg: { error: '获取用户列表失败' }, +// reducer: { name: 'members' } +// }); +// } export function getResource(userId) { return dispatch => basicAction({ type: 'get', @@ -44,7 +44,7 @@ export function postUserRes(body) { }); } export default { - getAuthority, + // getAuthority, getResource, getUserResource, postUserRes diff --git a/web/client/src/sections/publicityInfoConfig/actions/index.js b/web/client/src/sections/publicityInfoConfig/actions/index.js index 7ee4de2..7a5b3eb 100644 --- a/web/client/src/sections/publicityInfoConfig/actions/index.js +++ b/web/client/src/sections/publicityInfoConfig/actions/index.js @@ -2,7 +2,9 @@ import * as publicityInfoConfig from './publicityInfoConfig' +import * as label from './label' export default { ...publicityInfoConfig, + ...label, } \ No newline at end of file diff --git a/web/client/src/sections/publicityInfoConfig/actions/label.js b/web/client/src/sections/publicityInfoConfig/actions/label.js new file mode 100644 index 0000000..c3b1da0 --- /dev/null +++ b/web/client/src/sections/publicityInfoConfig/actions/label.js @@ -0,0 +1,38 @@ +'use strict'; + +import { basicAction } from '@peace/utils' +import { ApiTable } from '$utils' + +export function getLabels(query) { + return (dispatch) => basicAction({ + type: 'get', + query, + dispatch, + actionType: 'GET_LABELS', + url: ApiTable.getLabels, + msg: { error: '获取标签失败', }, + reducer: { name: 'labels' } + }); +} + +export function createLabels(data) { + return (dispatch) => basicAction({ + type: 'post', + data, + dispatch, + actionType: 'POST_LABELS', + url: ApiTable.createLabels, + msg: { option: '新增标签', }, + }); +} + +export function delLabels(id) { + return (dispatch) => basicAction({ + type: 'del', + dispatch, + actionType: 'DEL_LABELS', + url: ApiTable.delLabels.replace('{id}', id), + msg: { option: '删除标签', }, + }); +} + diff --git a/web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js b/web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js index f159551..3386879 100644 --- a/web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js +++ b/web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js @@ -3,82 +3,50 @@ import { basicAction } from '@peace/utils' import { ApiTable } from '$utils' - -export function getProjectList (query) { +export function getPublicityInfo(query) { return (dispatch) => basicAction({ type: 'get', query, dispatch, - actionType: 'GET_PROJEECT_LIST', - url: ApiTable.getProjectList, - msg: { error: '获取结构物列表失败', }, + actionType: 'GET_PUBLICITY_INFO', + url: ApiTable.getPublicityInfo, + msg: { error: '获取宣传信息失败', }, }); } - -export function postAddProject (data) { +export function createPublicityInfo(data) { return (dispatch) => basicAction({ type: 'post', data, dispatch, - actionType: 'POST_ADD_PROJECT', - url: ApiTable.postAddProject, - msg: { option: data?.id ? '编辑结构物' : '新增结构物', }, - }); -} - - -export function delProject (id) { - return (dispatch) => basicAction({ - type: 'del', - dispatch, - actionType: 'DEL_PROJECT', - url: ApiTable.delProject.replace('{id}', id), - msg: { - option: '删除结构物', - }, + actionType: 'POST_PUBLICITY_INFO', + url: ApiTable.createPublicityInfo, + msg: { option: '新增宣传信息', }, }); } - -export function addPosition (data, point) { +export function updatePublicityInfo(data, id) { return (dispatch) => basicAction({ - type: 'post', + type: 'put', data, dispatch, - actionType: 'ADD_POSITION', - url: ApiTable.position, - msg: { option: point ? '二维码生成' : data?.id ? '编辑点位' : '新增点位', }, - }); -} - -export function positionList (query) { - return (dispatch) => basicAction({ - type: 'get', - query, - dispatch, - actionType: 'POSITION_LIST', - url: ApiTable.position, - msg: { error: '获取点位列表失败', }, - reducer: { name: 'projectPoints' } + actionType: 'PUT_PUBLICITY_INFO', + url: ApiTable.updatePublicityInfo.replace('{id}', id), + msg: { option: '修改宣传信息', }, }); } - -export function delPosition (id) { +export function delPublicityInfo(id, qrcodeId) { return (dispatch) => basicAction({ type: 'del', dispatch, - actionType: 'DEL_POSITION', - url: ApiTable.delPosition.replace('{id}', id), - msg: { - option: '删除点位', - }, + actionType: 'DEL_PUBLICITY_INFO', + url: ApiTable.delPublicityInfo.replace('{id}', id).replace('{qrcodeId}', qrcodeId), + msg: { option: '删除宣传信息', }, }); } - -export function qrCodeShow (query) { +export function qrCodeShow(query) { return (dispatch) => basicAction({ type: 'get', query, @@ -88,14 +56,3 @@ export function qrCodeShow (query) { msg: { error: '获取二维码列表失败', }, }); } - -export function q () { - return (dispatch) => basicAction({ - type: 'get', - dispatch, - actionType: 'GET_CODE', - url: ApiTable.q, - msg: { error: '获取二维码列表失败', }, - }); -} - diff --git a/web/client/src/sections/publicityInfoConfig/components/publicityInfoModal.js b/web/client/src/sections/publicityInfoConfig/components/publicityInfoModal.js new file mode 100644 index 0000000..590c2a8 --- /dev/null +++ b/web/client/src/sections/publicityInfoConfig/components/publicityInfoModal.js @@ -0,0 +1,273 @@ +import React, { useState, useEffect } from 'react'; +import { Button, Form, Input, Modal, Select, Divider, Row, Col, Image, message } from 'antd'; +import QrcodeFilesUpload from '../../../components/QrcodeFilesUpload'; +import { Uploads } from '../../../components'; +import { connect } from 'react-redux'; +import { getLabels } from '../actions/label'; +import { createPublicityInfo, updatePublicityInfo } from '../actions/publicityInfoConfig'; +import QrCodeWithLogo from 'qr-code-with-logo' +import { v4 as uuidv4 } from 'uuid'; +import request from 'superagent'; + +const PublicityInfoModal = ({ visible, onCancel, updateList, dispatch, curRecord, labels }) => { + const [form] = Form.useForm(); + + const [type, setType] = useState('图片'); + const [qrcode, setQrcode] = useState(''); + + useEffect(() => { + getLabelsData() + if (curRecord) { + console.log(curRecord, 'curRecord') + setType(curRecord.type) + } else { + draw() + } + }, []) + + const getLabelsData = () => { + dispatch(getLabels({})); + } + + const draw = () => { + const ctx = document.getElementById("qrcodeCanvas").getContext("2d"); + ctx.fillStyle = "#f5f5f5"; + ctx.fillRect(0, 0, 220, 220); + ctx.fillStyle = "black"; + ctx.font = "14px serif"; + ctx.fillText("此处预览二维码", 60, 110); + } + + const handleSubmit = async (values) => { + console.log(values, 'values') + if (curRecord) { + // dispatch(updatePublicityInfo(params)).then(res => { + // if (res.success) { + // // form.resetFields(); + // // onCancel(); + // } + // }) + } else { + const key = uuidv4(); + // 生成二维码 + let qrcodeUrl = ''; + const qrcodeCanvas = document.getElementById('qrcodeCanvas') + let options = { + canvas: qrcodeCanvas, + width: 220, + content: values.type === '链接' + ? values.link + : `${window.location.protocol}//${window.location.host}/scan?key=${key}`, + } + if (values.qrcodeImg?.length) { + options.logo = { + src: '/_file-server/' + values.qrcodeImg[0].storageUrl, + radius: 8 + } + } + await QrCodeWithLogo.toCanvas(options) + + // 上传二维码 + try { + const blob = await canvasToBlob(qrcodeCanvas); + const formData = new FormData(); + formData.append('image', blob, values.name + '.png'); + const res = await request.post('/_upload/attachments/image', formData) + qrcodeUrl = res.body.uploaded + } catch (error) { + console.log(error); + message.error('二维码上传失败'); + return; + } + console.log(qrcodeUrl, 'qrcodeUrl') + dispatch(createPublicityInfo({ + ...values, + qrcode: { url: qrcodeUrl, key }, + })).then(res => { + if (res.success) { + form.resetFields(); + onCancel(); + updateList(); + } + }) + } + } + + const handleChange = (value) => { + console.log(`selected ${value}`); + }; + + return ( + { + form.validateFields() + .then((values) => { + handleSubmit(values) + }) + .catch((info) => { + console.log('Validate Failed:', info); + }); + }}> + 生成二维码并保存 + , + // , + , + ]} + onCancel={() => { + form.resetFields(); + onCancel(); + }} + > +
{ + }} + > + + + + + + : + { + return { + storageUrl: s + } + })} + /> + } + + + + + { + return { + storageUrl: s + } + })} + /> + + + + @@ -111,22 +111,22 @@ const QrCode = (props) => { borderRadius: '3px' }} > - +
{firmList?.filter(u => u.value == v.projectId)[0]?.label} - 点位名称:{v.name} + 名称:{v.name}
diff --git a/web/client/src/sections/qrCode/containers/scanCode.js b/web/client/src/sections/qrCode/containers/scanCode.js new file mode 100644 index 0000000..8f95275 --- /dev/null +++ b/web/client/src/sections/qrCode/containers/scanCode.js @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from 'react'; +import { connect } from 'react-redux'; +import { SafeArea, AutoCenter, Button, Card, Toast, Image, Popup, Tag, Space, WaterMark, Divider } from 'antd-mobile'; +import { getQrCode } from '../actions/qrCode'; +import './scanCode.less'; + +const fileType = { + report: ['doc', 'docx', 'xls', 'xlsx', 'csv', 'pdf', 'pptx'], + image: ['png', 'jpg', 'svg', 'jpeg'], + video: ['mp4'] +}; + +const ScanCode = ({ dispatch }) => { + const urlParams = new URLSearchParams(window.location.search); + const key = urlParams.get('key'); + const qnDomain = localStorage.getItem('qnDomain'); + + const [qrCode, setQrCode] = useState({}); + const [visible, setVisible] = useState(false) + const [link, setLink] = useState('') + + useEffect(() => { + if (!key) { + Toast.show({ + icon: 'fail', + content: '二维码参数错误', + }) + return + } + getQrCodeData(key) + }, []) + + const getQrCodeData = (key) => { + dispatch(getQrCode({ key })).then(res => { + console.log(res, 'res') + if (!res.payload?.data?.rows?.length) { + Toast.show({ icon: 'fail', content: '该二维码已被删除,无法访问' }) + return + } + setQrCode(res.payload.data.rows[0]) + }) + } + + const { qrcodeFiles } = qrCode; + + return (<> + + {qrCode.name} + + { + qrcodeFiles?.map((f, i) => { + const extensionName = getFileExtension(f.fileUrl) + if (fileType.report.includes(extensionName)) { + return +
{f.fileName}
+ {extensionName} + } + extra={} + > + { + // const link = encodeURI(`https://view.officeapps.live.com/op/view.aspx?src=${qnDomain}/${f.fileUrl}`); + const nextLink = encodeURI(`https://view.xdocin.com/view?src=${qnDomain}/${f.fileUrl}`); + setLink(nextLink) + setVisible(true) + }} + /> +
+ } + else if (fileType.image.includes(extensionName)) { + return +
{f.fileName}
+ {extensionName} + }> + +
+ } + else if (fileType.video.includes(extensionName)) { + return +
{f.fileName}
+ {extensionName} + }> +
+ } + }) + } + { + setVisible(false) + }} + position='bottom' + bodyStyle={{ height: '80vh' }} + closeOnMaskClick + > + + + + + ) +} + +function mapStateToProps(state) { + const { global } = state; + return { + actions: global.actions, + }; +} + +export default connect(mapStateToProps)(ScanCode); + +const getFileExtension = (fileUrl) => { + const fileNameWithExtension = fileUrl.split('/').pop(); // 获取URL的最后一部分(即文件名和扩展名) + const extension = fileNameWithExtension.split('.').pop(); // 从文件名中分离出扩展名 + + // 如果没有扩展名或文件名不含点,则返回空字符串 + return extension ? extension.toLowerCase() : ''; +}; \ No newline at end of file diff --git a/web/client/src/sections/qrCode/containers/scanCode.less b/web/client/src/sections/qrCode/containers/scanCode.less new file mode 100644 index 0000000..842ed88 --- /dev/null +++ b/web/client/src/sections/qrCode/containers/scanCode.less @@ -0,0 +1,16 @@ +// @import '~antd-mobile/dist/antd-mobile.css'; + +html { + font-size: 24px; +} + +body { + font-size: 1rem; +} + +.text--- { + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + white-space: nowrap; +} \ No newline at end of file diff --git a/web/client/src/sections/qrCode/routes.js b/web/client/src/sections/qrCode/routes.js index 5c9960e..187c8f1 100644 --- a/web/client/src/sections/qrCode/routes.js +++ b/web/client/src/sections/qrCode/routes.js @@ -1,21 +1,32 @@ 'use strict'; -import { QrCode, } from './containers'; +import { QrCode, ScanCode } from './containers'; -export default [{ - type: 'inner', - route: { - path: '/qrCode', - key: 'qrCode', - breadcrumb: '二维码管理', - component: QrCode, - // 不设置 component 则面包屑禁止跳转 - // childRoutes: [ - // { - // path: '/qrCode', - // key: 'qrCode', - // component: QrCode, - // breadcrumb: '二维码管理', - // }, - // ] +export default [ + { + type: 'inner', + route: { + path: '/qrCode', + key: 'qrCode', + breadcrumb: '二维码管理', + component: QrCode, + // 不设置 component 则面包屑禁止跳转 + // childRoutes: [ + // { + // path: '/qrCode', + // key: 'qrCode', + // component: QrCode, + // breadcrumb: '二维码管理', + // }, + // ] + } + }, + { + type: 'outer', + route: { + path: '/scan', + key: 'scan', + component: ScanCode, + breadcrumb: '扫码页面', + } } -}]; \ No newline at end of file +]; \ No newline at end of file diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js index b54b2f0..2245165 100644 --- a/web/client/src/utils/webapi.js +++ b/web/client/src/utils/webapi.js @@ -4,8 +4,6 @@ import request from 'superagent'; export const ApiTable = { login: 'login', logout: 'logout', - validatePhone: 'validate/phone', - getUserSiteList: 'user/site/list', // 组织管理-用户管理 getDepMessage: 'organization/department', @@ -19,131 +17,24 @@ export const ApiTable = { delUser: 'organization/department/user/{ids}', resetPwd: '/organization/department/user/resetPwd/{id}', - // 巡检计划 - patrolPlan: 'patrolPlan', // 增改查 - delPatrolPlan: 'patrolPlan/{id}', - - // 巡检模板 - patrolTemplate: 'patrolTemplate', // 增改查 - delPatrolTemplate: 'patrolTemplate/{id}', - - // 巡检记录 - patrolRecord: 'patrolRecord/:patrolPlanId/:startTime/:endTime/:alarm/:pointId', - - // 巡检报告 - patrolReport: 'patrolReport', - - // 检查项设定 - checkItems: 'checkItems', // 获取/新增 - updateCheckItems: 'checkItems/{id}', - delCheckItems: 'checkItems/{ids}', - checkItemsGroup: 'checkItems/group', // 获取/新增 - delCheckItemsGroup: 'checkItems/group/{id}', - // 用户权限 getResource: 'resource', getUserResource: 'user/resource', postUserRes: 'user/resource', - //安全风险预报 - getSiteWeekRegiste: 'sites/report', - getRiskReportList: 'risk/report', - modifyRiskReport: 'risk/report/{riskId}', - riskExport: 'risk/export', - getEnterprisesMembers: 'enterprises/{enterpriseId}/members', - - //工程交底 - getProjectDisclosureList: 'project/disclosure', - addProjectDisclosure: 'project/disclosure', - editProjectDisclosure: 'project/disclosure/{id}', - delProjectDisclosure: 'project/disclosure/{id}', - - //安全巡检 - getCheckTask: '/getcheckTask', - addCheckTask: '/addcheckTask', - editCheckTask: '/editcheckTask', - delCheckTask: '/delcheckTask/:id', - addPatrolRecordIssueHandle: 'patrolRecord/issue/handle', - modifyPatrolRecordIssueHandle: 'patrolRecord/issue/handle/{id}', - yujingguanli: '/yujingguanli', - - //协调申请 - getCoordinateList: 'risk/coordinate', - addCoordinate: 'risk/coordinate', - delCoordinate: 'risk/coordinate/{id}', - editCoordinate: 'risk/coordinate/{id}', - - //会议 - mettingList: 'metting/list', - editMetting: 'metting', - - //隐患整改 - getRectifyList: 'rectify/list', - addRectify: 'rectify', - editRectify: 'rectify/{id}', - delRectify: 'rectify/{id}', - disposeRectify: 'rectify/dispose', - rectifyReport: 'rectify/report', - - //问题上报 - problemReport: 'report/problem', - //查阅人员 - problemReportConsult: 'report/problem/consult', - - //花名册管理 - getWorkerList: 'get/worker/list', - addWorker: 'add/worker', - editWorker: 'worker/{id}', - delWorker: 'worker/{id}', - getWorkerIdcards: 'worker/idcards', - //安全管理 - getTring: 'training/list/:siteid', - postTring: 'training', - putTring: 'training', - delTring: 'training/:id', - - //考情信息 - getChcekList: 'get/worker/attendance/list', - addCheck: 'add/worker/attendance', - editCheck: 'worker/attendance/:id', - delCheck: 'worker/attendance/:id', - verifyWoker: 'verify/worker/exist', - - //首页-我的待办 - getDealTodoList: 'user/deal/list', - addDealTodo: 'user/deal', - - //结构物 - getProjectList: 'projectList', - postAddProject: 'addProject', - delProject: 'delProject/{id}', - - //点位 - position: 'position', - delPosition: 'delPosition/{id}', - qrCodeShow: 'qrCodeShow', - q: 'q', - - //视频接入配置 - siteList: 'siteList', - addCamera: 'camera', - delCamera: 'camera/{id}', + // 标签 + getLabels: 'label', + createLabels: 'label/add', + delLabels: 'label/{id}', - //项目状态配置 - editProjectStatus: 'project/status', - //工地平面图 - getProjectGraph: 'project/{projectId}/planarGraph', - createGraph: 'planarGraph/add', - updateGraph: 'planarGraph/{id}/modify', - deleteGraph: 'project/graph/{id}', - getProjectPoints: 'project/{projectId}/all/points', - getDeployPoints: 'picture/{pictureId}/deploy/points', - setDeployPoints: 'set/picture/{pictureId}/deploy/points', + // 宣传信息配置 + getPublicityInfo: 'publicityInfo', + createPublicityInfo: 'publicityInfo/add', + updatePublicityInfo: 'publicityInfo/{id}/modify', + delPublicityInfo: 'publicityInfo/{id}/del?qrcodeId={qrcodeId}', - //设备管理 - getDeviceList: 'device', - addDevice: 'device', - modifyDevice: 'device/{id}', + // 二维码 + getQrCode: 'qrcode', }; export const RouteTable = { diff --git a/web/package.json b/web/package.json index f2ee917..5b69bd4 100644 --- a/web/package.json +++ b/web/package.json @@ -72,6 +72,7 @@ "ahooks": "^3.7.4", "ali-oss": "^6.17.1", "antd": "^4.24.5", + "antd-mobile": "^5.34.0", "antd-theme-generator": "^1.2.8", "args": "^5.0.1", "array-move": "^3.0.1", @@ -94,11 +95,12 @@ "mini-dynamic-antd-theme": "^0.5.3", "moment": "^2.22.0", "npm": "^7.20.6", + "qr-code-with-logo": "^1.1.0", "qrcode": "^1.5.1", "qs": "^6.10.1", + "react-color": "^2.19.3", "react-dnd": "^7", "react-dnd-html5-backend": "^7", - "react-color": "^2.19.3", "react-router-breadcrumbs-hoc": "^4.0.1", "react-sortable-hoc": "^2.0.0", "shortid": "^2.2.16", diff --git a/web/webpack.config.js b/web/webpack.config.js index 7f421c8..37fb881 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -37,6 +37,7 @@ module.exports = { module: { rules: [{ test: /\.css$/, + exclude: path.resolve(__dirname, 'node_modules', 'antd-mobile'), use: ['style-loader', { loader: 'css-loader', options: { @@ -44,6 +45,16 @@ module.exports = { } }] }, + { + test: /\.css$/, + include: path.resolve(__dirname, 'node_modules', 'antd-mobile'), + use: ['style-loader', { + loader: 'css-loader', + options: { + modules: false + } + }] + }, { test: /\.less$/, use: ['style-loader', 'css-loader', { diff --git a/web/webpack.config.prod.js b/web/webpack.config.prod.js index af8ef36..3a41abc 100644 --- a/web/webpack.config.prod.js +++ b/web/webpack.config.prod.js @@ -46,6 +46,7 @@ module.exports = { module: { rules: [{ test: /\.css$/, + exclude: path.resolve(__dirname, 'node_modules', 'antd-mobile'), use: ['style-loader', { loader: 'css-loader', options: { @@ -53,6 +54,16 @@ module.exports = { } }] }, + { + test: /\.css$/, + include: path.resolve(__dirname, 'node_modules', 'antd-mobile'), + use: ['style-loader', { + loader: 'css-loader', + options: { + modules: false + } + }] + }, { test: /\.less$/, use: ['style-loader', 'css-loader', @@ -68,7 +79,7 @@ module.exports = { test: /\.jsx?$/, use: 'babel-loader', include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], - },{ + }, { test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, loader: "file-loader" }]