Browse Source

(+) 二维码基本功能实现

master
liujiangyong 9 months ago
parent
commit
c12a43acdc
  1. 71
      api/app/lib/controllers/label/index.js
  2. 173
      api/app/lib/controllers/publicityInfo/index.js
  3. 56
      api/app/lib/controllers/qrcode/index.js
  4. 16
      api/app/lib/index.js
  5. 11
      api/app/lib/middlewares/authenticator.js
  6. 74
      api/app/lib/models/publicity_info.js
  7. 70
      api/app/lib/models/qrcode.js
  8. 87
      api/app/lib/models/qrcode_files.js
  9. 34
      api/app/lib/models/qrcode_labels.js
  10. 64
      api/app/lib/models/qrcode_labels_qrcode.js
  11. 15
      api/app/lib/routes/label/index.js
  12. 18
      api/app/lib/routes/publicityInfo/index.js
  13. 13
      api/app/lib/routes/qrcode/index.js
  14. 47
      script/1.0.0/schema/2.init_qrcode.sql
  15. 1
      web/client/assets/images/document.svg
  16. BIN
      web/client/assets/images/logo.png
  17. 9
      web/client/assets/images/logo.svg
  18. BIN
      web/client/assets/images/logo_white.png
  19. 5
      web/client/index.ejs
  20. 7
      web/client/index.html
  21. 52
      web/client/src/components/QrcodeFilesUpload/fileInfoForm.js
  22. 413
      web/client/src/components/QrcodeFilesUpload/index.js
  23. 0
      web/client/src/components/QrcodeFilesUpload/index.less
  24. 2
      web/client/src/layout/components/header/index.js
  25. 22
      web/client/src/sections/organization/actions/authority.js
  26. 2
      web/client/src/sections/publicityInfoConfig/actions/index.js
  27. 38
      web/client/src/sections/publicityInfoConfig/actions/label.js
  28. 79
      web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js
  29. 273
      web/client/src/sections/publicityInfoConfig/components/publicityInfoModal.js
  30. 145
      web/client/src/sections/publicityInfoConfig/containers/publicityInfoConfig.js
  31. 89
      web/client/src/sections/qrCode/actions/qrCode.js
  32. 3
      web/client/src/sections/qrCode/containers/index.js
  33. 18
      web/client/src/sections/qrCode/containers/qrCode.js
  34. 141
      web/client/src/sections/qrCode/containers/scanCode.js
  35. 16
      web/client/src/sections/qrCode/containers/scanCode.less
  36. 47
      web/client/src/sections/qrCode/routes.js
  37. 131
      web/client/src/utils/webapi.js
  38. 4
      web/package.json
  39. 11
      web/webpack.config.js
  40. 13
      web/webpack.config.prod.js

71
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,
}

173
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,
}

56
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,
}

16
api/app/lib/index.js

@ -53,7 +53,10 @@ module.exports.models = function (dc) { // dc = { orm: Sequelize对象, ORM: Seq
require(`./models/${filename}`)(dc) 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' }); UserResource.belongsTo(User, { foreignKey: 'userId', targetKey: 'id' });
User.hasMany(UserResource, { foreignKey: 'userId', sourceKey: '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' }); User.belongsTo(Department, { foreignKey: 'departmentId', targetKey: 'id' });
Department.hasMany(User, { foreignKey: 'departmentId', sourceKey: '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' });
}; };

11
api/app/lib/middlewares/authenticator.js

@ -13,13 +13,13 @@ class ExcludesUrls {
this.reload(opts); this.reload(opts);
} }
sanitizePath (path) { sanitizePath(path) {
if (!path) return '/'; if (!path) return '/';
const p = '/' + path.replace(/^\/+/i, '').replace(/\/+$/, '').replace(/\/{2,}/, '/'); const p = '/' + path.replace(/^\/+/i, '').replace(/\/+$/, '').replace(/\/{2,}/, '/');
return p; return p;
} }
reload (opts) { reload(opts) {
// load all url // load all url
if (!this.allUrls) { if (!this.allUrls) {
this.allUrls = opts; this.allUrls = opts;
@ -37,7 +37,7 @@ class ExcludesUrls {
} }
} }
isExcluded (path, method) { isExcluded(path, method) {
return this.allUrls.some(function (url) { return this.allUrls.some(function (url) {
return !url.auth return !url.auth
&& url.pregexp.test(path) && url.pregexp.test(path)
@ -59,6 +59,7 @@ let isPathExcluded = function (opts, path, method) {
let excludeOpts = opts.exclude || []; let excludeOpts = opts.exclude || [];
excludeOpts.push({ p: '/login', o: 'POST' }); excludeOpts.push({ p: '/login', o: 'POST' });
excludeOpts.push({ p: '/logout', o: 'PUT' }); excludeOpts.push({ p: '/logout', o: 'PUT' });
excludeOpts.push({ p: '/qrcode', o: 'GET' });
excludes = new ExcludesUrls(excludeOpts); excludes = new ExcludesUrls(excludeOpts);
} }
let excluded = excludeAll || excludes.isExcluded(path, method); let excluded = excludeAll || excludes.isExcluded(path, method);
@ -110,8 +111,8 @@ let isResourceAvailable = function (resources, options) {
return !authCode || (resources || []).some(code => code === authCode); return !authCode || (resources || []).some(code => code === authCode);
}; };
function factory (app, opts) { function factory(app, opts) {
return async function auth (ctx, next) { return async function auth(ctx, next) {
const { path, method, header, query } = ctx; const { path, method, header, query } = ctx;
ctx.fs.logger.log('[AUTH] start', path, method); ctx.fs.logger.log('[AUTH] start', path, method);
ctx.fs.api = ctx.fs.api || {}; ctx.fs.api = ctx.fs.api || {};

74
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;
};

70
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;
};

87
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;
};

34
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;
};

64
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;
};

15
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);
};

18
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);
};

13
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);
};

47
script/1.0.0/schema/2.init_qrcode.sql

@ -4,11 +4,15 @@ CREATE TABLE "public"."qrcode" (
"url" varchar(255) NOT NULL, "url" varchar(255) NOT NULL,
"name" varchar(128) NOT NULL, "name" varchar(128) NOT NULL,
"type" varchar(32) NOT NULL, "type" varchar(32) NOT NULL,
"key" uuid NOT NULL,
"logo" varchar(255),
PRIMARY KEY ("id") 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"."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"; DROP TABLE IF EXISTS "public"."publicity_info";
@ -17,27 +21,56 @@ CREATE TABLE "public"."publicity_info" (
"name" varchar(128) NOT NULL, "name" varchar(128) NOT NULL,
"type" varchar(32) NOT NULL, "type" varchar(32) NOT NULL,
"time" timestamptz NOT NULL, "time" timestamptz NOT NULL,
"qrcode_id" int4 NOT NULL, "link" varchar(255),
"qrcode_id" int4,
PRIMARY KEY ("id"), PRIMARY KEY ("id"),
CONSTRAINT "publicity_info_qrcode_id_fk" FOREIGN KEY ("qrcode_id") REFERENCES "public"."qrcode" ("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"."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"."time" IS '创建/更新时间';
COMMENT ON COLUMN "public"."publicity_info"."link" IS 'type为链接时的链接地址';
COMMENT ON COLUMN "public"."publicity_info"."qrcode_id" IS '关联二维码ID'; COMMENT ON COLUMN "public"."publicity_info"."qrcode_id" IS '关联二维码ID';
DROP TABLE IF EXISTS "public"."qrcode_files"; DROP TABLE IF EXISTS "public"."qrcode_files";
CREATE TABLE "public"."qrcode_files" ( CREATE TABLE "public"."qrcode_files" (
"id" serial, "id" serial,
"qrcode_id" int4 NOT NULL,
"file_name" varchar(128) NOT NULL, "file_name" varchar(128) NOT NULL,
"file_size" int4 NOT NULL, "file_size" int4 NOT NULL,
"file_url" varchar(255) NOT NULL, "file_url" varchar(255) NOT NULL,
"preview_img_url" varchar(255),
"qrcode_id" int4,
"publicity_info_id" int4,
PRIMARY KEY ("id"), 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_name" IS '文件名';
COMMENT ON COLUMN "public"."qrcode_files"."file_size" IS '文件大小(byte)'; 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"."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")
);

1
web/client/assets/images/document.svg

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1706165283537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1952" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M746.602 0.118H106.75v1023.764h810.5V170.734z" fill="#E6E9ED" p-id="1953"></path><path d="M234.72 341.368l97.508-162.51 97.51 162.51z" fill="#4A89DC" p-id="1954"></path><path d="M315.982 341.368l130.002-227.51 130 227.51z" fill="#5D9CEC" p-id="1955"></path><path d="M767.942 597.292H256.058c-11.78 0-21.338-9.53-21.338-21.308 0-11.808 9.558-21.338 21.338-21.338H767.94c11.778 0 21.338 9.53 21.338 21.338 0.002 11.78-9.56 21.308-21.336 21.308zM767.942 725.264H256.058c-11.78 0-21.338-9.53-21.338-21.306 0-11.81 9.558-21.34 21.338-21.34H767.94a21.328 21.328 0 0 1 21.338 21.34c0.002 11.776-9.56 21.306-21.336 21.306zM533.338 853.234h-277.28c-11.78 0-21.338-9.53-21.338-21.308 0-11.808 9.558-21.338 21.338-21.338h277.28c11.78 0 21.306 9.53 21.306 21.338 0 11.78-9.526 21.308-21.306 21.308z" fill="#434A54" p-id="1956"></path><path d="M746.602 0.118v170.616h170.648z" fill="#CCD1D9" p-id="1957"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
web/client/assets/images/logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 22 KiB

9
web/client/assets/images/logo.svg

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.0 MiB

BIN
web/client/assets/images/logo_white.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

5
web/client/index.ejs

@ -3,13 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<link rel="shortcut icon" href="/assets/images/logo.png"> <link rel="shortcut icon" href="/assets/images/logo.png">
<link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css"> <link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css">
<script type="text/javascript"> <!-- <script type="text/javascript">
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: 'e955cd5ddfc3a752aa27d1e1c67d182d', securityJsCode: 'e955cd5ddfc3a752aa27d1e1c67d182d',
} }
</script> </script> -->
<!-- <script src="https://webapi.amap.com/maps?v=2.0&key=00f9a29dedcdbd8befec3dfe0cef5003&plugin=AMap.AutoComplete,AMap.PlaceSearch"></script> --> <!-- <script src="https://webapi.amap.com/maps?v=2.0&key=00f9a29dedcdbd8befec3dfe0cef5003&plugin=AMap.AutoComplete,AMap.PlaceSearch"></script> -->
<!-- <script src="https://webapi.amap.com/loca?v=2.0.0&key=00f9a29dedcdbd8befec3dfe0cef5003"></script> --> <!-- <script src="https://webapi.amap.com/loca?v=2.0.0&key=00f9a29dedcdbd8befec3dfe0cef5003"></script> -->
</head> </head>

7
web/client/index.html

@ -3,14 +3,15 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />
<title></title> <title></title>
<link rel="shortcut icon" href="/assets/images/logo.svg"> <link rel="shortcut icon" href="/assets/images/logo.png">
<link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css"> <link rel="stylesheet" type="text/css" href="/assets/font_sc/iconfont.css">
<script type="text/javascript"> <!-- <script type="text/javascript">
window._AMapSecurityConfig = { window._AMapSecurityConfig = {
securityJsCode: 'e955cd5ddfc3a752aa27d1e1c67d182d', securityJsCode: 'e955cd5ddfc3a752aa27d1e1c67d182d',
} }
</script> </script> -->
<!-- <script src="https://webapi.amap.com/maps?v=2.0&key=00f9a29dedcdbd8befec3dfe0cef5003&plugin=AMap.AutoComplete,AMap.PlaceSearch"></script> --> <!-- <script src="https://webapi.amap.com/maps?v=2.0&key=00f9a29dedcdbd8befec3dfe0cef5003&plugin=AMap.AutoComplete,AMap.PlaceSearch"></script> -->
<!-- <script src="https://webapi.amap.com/loca?v=2.0.0&key=00f9a29dedcdbd8befec3dfe0cef5003"></script> --> <!-- <script src="https://webapi.amap.com/loca?v=2.0.0&key=00f9a29dedcdbd8befec3dfe0cef5003"></script> -->
</head> </head>

52
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 (
<Form
form={form}
layout="inline"
name="preview_form"
initialValues={{}}
>
<Form.Item
name="fileName"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]}
>
<Input style={{ width: '200px' }} />
</Form.Item>
<Form.Item
name="previewImg"
label="预览图上传"
rules={[]}
>
<Uploads
listType='picture-card'
uploadType='image'
maxFilesNum={1}
maxFileSize={5}
isQiniu={true}
fileTypes={["png", "jpg"]}
defaultValue={([]).map(s => {
return {
storageUrl: s
}
})}
/>
</Form.Item>
</Form>
)
}

413
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 (
<div>
<Spin spinning={delPicIng}>
<Upload
{...uploadProps}
fileList={fileList_}
showUploadList={{ showDownloadIcon: true }}
onDownload={handleDownload}
itemRender={(originNode, file, currFileList) => {
const fileName = file.name?.split('.').slice(0, -1).join('.')
const extension = file.name?.split('.').pop()
return <>
{originNode}
<Form
layout="inline"
name="preview_form"
initialValues={{
fileName: fileName
}}
>
<Form.Item
name="fileName"
label="文件标题"
rules={[]}
>
<Input
onChange={({ target: { value } }) => {
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' }}
/>
</Form.Item>
{!UploadPath.image.includes(extension) && <Form.Item
name="previewImg"
label="预览图上传"
rules={[]}
>
<Uploads
listType='picture-card'
uploadType='image'
maxFilesNum={1}
maxFileSize={5}
isQiniu={true}
fileTypes={["png", "jpg", "jpeg"]}
onChange={(previewFile) => {
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
}
})}
/>
</Form.Item>}
</Form>
</>
}}
>
{
disabled ? ('') :
listType == 'picture-card' ?
(
fileList.length >= maxFilesNum_ ? null : (
<div style={{}}>
<PlusOutlined />
<div>添加附件</div>
</div>
)
) : (
<Button disabled={fileList.length >= maxFilesNum_} icon={<UploadOutlined />}> 文件上传 </Button>
)
}
</Upload>
{
curPreviewPic ? (
<Card bodyStyle={{ padding: 8 }}>
<div style={{ marginBottom: 8 }} >
<span>图片预览</span>
<span style={{ float: 'right' }} onClick={() => { this.setState({ curPreviewPic: '' }) }}>
<CloseOutlined style={{ fontSize: 20 }} />
</span>
</div>
<img style={{ width: '100%' }} src={curPreviewPic} />
</Card>
) : ''
}
{
curPreviewVideo ? (<Card bodyStyle={{ padding: 8 }}>
<div style={{ marginBottom: 8 }} >
<span>视频预览</span>
<span style={{ float: 'right' }} onClick={() => { this.setState({ curPreviewVideo: '' }) }}>
<CloseOutlined style={{ fontSize: 20 }} />
</span>
</div>
<video controls style={{ width: '100%' }}>
<source src={curPreviewVideo} type="video/mp4"></source>
</video>
</Card>) : ''
}
</Spin>
</div>
);
}
}
function mapStateToProps(state) {
const { auth } = state
return {
user: auth.user
};
}
export default connect(mapStateToProps)(QrcodeFilesUpload);

0
web/client/src/components/QrcodeFilesUpload/index.less

2
web/client/src/layout/components/header/index.js

@ -29,7 +29,7 @@ const Header = props => {
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles['header-fold']}> <div className={styles['header-fold']}>
<img src='/assets/images/logo.svg' style={{ margin: '-14px 12px 0 0', height: 24 }} /> <img src='/assets/images/logo_white.png' style={{ margin: '-14px 12px 0 0', height: 36 }} />
<div className={styles['header-title']} style={{}}> <div className={styles['header-title']} style={{}}>
<div className={styles['title-cn']}>飞尚码尚来二维码管理系统</div> <div className={styles['title-cn']}>飞尚码尚来二维码管理系统</div>
<div className={styles['title-en']}>FREESUN QRCODE MANAGEMENT SYSTEM</div> <div className={styles['title-en']}>FREESUN QRCODE MANAGEMENT SYSTEM</div>

22
web/client/src/sections/organization/actions/authority.js

@ -3,16 +3,16 @@
import { basicAction } from '@peace/utils' import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils' import { ApiTable } from '$utils'
export function getAuthority(orgId) { // export function getAuthority(orgId) {
return dispatch => basicAction({ // return dispatch => basicAction({
type: 'get', // type: 'get',
dispatch: dispatch, // dispatch: dispatch,
actionType: 'GET_MEMBERS', // actionType: 'GET_MEMBERS',
url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`, // url: `${ApiTable.getEnterprisesMembers.replace('{enterpriseId}', orgId)}`,
msg: { error: '获取用户列表失败' }, // msg: { error: '获取用户列表失败' },
reducer: { name: 'members' } // reducer: { name: 'members' }
}); // });
} // }
export function getResource(userId) { export function getResource(userId) {
return dispatch => basicAction({ return dispatch => basicAction({
type: 'get', type: 'get',
@ -44,7 +44,7 @@ export function postUserRes(body) {
}); });
} }
export default { export default {
getAuthority, // getAuthority,
getResource, getResource,
getUserResource, getUserResource,
postUserRes postUserRes

2
web/client/src/sections/publicityInfoConfig/actions/index.js

@ -2,7 +2,9 @@
import * as publicityInfoConfig from './publicityInfoConfig' import * as publicityInfoConfig from './publicityInfoConfig'
import * as label from './label'
export default { export default {
...publicityInfoConfig, ...publicityInfoConfig,
...label,
} }

38
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: '删除标签', },
});
}

79
web/client/src/sections/publicityInfoConfig/actions/publicityInfoConfig.js

@ -3,82 +3,50 @@
import { basicAction } from '@peace/utils' import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils' import { ApiTable } from '$utils'
export function getPublicityInfo(query) {
export function getProjectList (query) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'get', type: 'get',
query, query,
dispatch, dispatch,
actionType: 'GET_PROJEECT_LIST', actionType: 'GET_PUBLICITY_INFO',
url: ApiTable.getProjectList, url: ApiTable.getPublicityInfo,
msg: { error: '获取结构物列表失败', }, msg: { error: '获取宣传信息失败', },
}); });
} }
export function createPublicityInfo(data) {
export function postAddProject (data) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'post', type: 'post',
data, data,
dispatch, dispatch,
actionType: 'POST_ADD_PROJECT', actionType: 'POST_PUBLICITY_INFO',
url: ApiTable.postAddProject, url: ApiTable.createPublicityInfo,
msg: { option: data?.id ? '编辑结构物' : '新增结构物', }, msg: { option: '新增宣传信息', },
});
}
export function delProject (id) {
return (dispatch) => basicAction({
type: 'del',
dispatch,
actionType: 'DEL_PROJECT',
url: ApiTable.delProject.replace('{id}', id),
msg: {
option: '删除结构物',
},
}); });
} }
export function updatePublicityInfo(data, id) {
export function addPosition (data, point) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'post', type: 'put',
data, data,
dispatch, dispatch,
actionType: 'ADD_POSITION', actionType: 'PUT_PUBLICITY_INFO',
url: ApiTable.position, url: ApiTable.updatePublicityInfo.replace('{id}', id),
msg: { option: point ? '二维码生成' : data?.id ? '编辑点位' : '新增点位', }, msg: { option: '修改宣传信息', },
});
}
export function positionList (query) {
return (dispatch) => basicAction({
type: 'get',
query,
dispatch,
actionType: 'POSITION_LIST',
url: ApiTable.position,
msg: { error: '获取点位列表失败', },
reducer: { name: 'projectPoints' }
}); });
} }
export function delPublicityInfo(id, qrcodeId) {
export function delPosition (id) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'del', type: 'del',
dispatch, dispatch,
actionType: 'DEL_POSITION', actionType: 'DEL_PUBLICITY_INFO',
url: ApiTable.delPosition.replace('{id}', id), url: ApiTable.delPublicityInfo.replace('{id}', id).replace('{qrcodeId}', qrcodeId),
msg: { msg: { option: '删除宣传信息', },
option: '删除点位',
},
}); });
} }
export function qrCodeShow(query) {
export function qrCodeShow (query) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'get', type: 'get',
query, query,
@ -88,14 +56,3 @@ export function qrCodeShow (query) {
msg: { error: '获取二维码列表失败', }, msg: { error: '获取二维码列表失败', },
}); });
} }
export function q () {
return (dispatch) => basicAction({
type: 'get',
dispatch,
actionType: 'GET_CODE',
url: ApiTable.q,
msg: { error: '获取二维码列表失败', },
});
}

273
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 (
<Modal
width={700}
visible={visible}
title={`${curRecord ? "编辑" : "新增"}宣传项`}
okText="确定"
cancelText="取消"
footer={[
<Button key="submit" type="primary" onClick={() => {
form.validateFields()
.then((values) => {
handleSubmit(values)
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}>
生成二维码并保存
</Button>,
// <Button type="primary" onClick={() => { }}>
// 生成二维码
// </Button>,
<Button key="back" onClick={() => {
form.resetFields();
onCancel();
}}>取消</Button>,
]}
onCancel={() => {
form.resetFields();
onCancel();
}}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={{
...curRecord,
type: curRecord?.type || type,
}}
onValuesChange={() => {
}}
>
<Form.Item
name="name"
label="宣传标题"
rules={[{ required: true, message: '请输入宣传标题' }]}
>
<Input />
</Form.Item>
<Form.Item
name="type"
label="类型"
rules={[{ required: true, message: '请选择类型' }]}
>
<Select
value={type}
options={[
{
value: '图片',
label: '图片',
},
{
value: '文件',
label: '文件',
},
{
value: '链接',
label: '链接',
},
{
value: '视频',
label: '视频',
},
]}
style={{ width: '50%' }}
onChange={(value) => { setType(value); }}
/>
</Form.Item>
{type === '链接' ? <Form.Item
name="link"
label="链接地址"
rules={[{ required: true, message: '请填写链接地址' }]}
>
<Input placeholder='http://' />
</Form.Item> : <Form.Item
name="files"
label={`${type}上传`}
rules={[{ required: true, message: '请上传文件' }]}
>
<QrcodeFilesUpload
listType='list'
maxFilesNum={50}
maxFileSize={500}
isQiniu={true}
uploadType={type === '图片' ? 'image' : type === '视频' ? 'video' : 'project'}
// fileTypes={["png", "jpg", "mp4"]}
defaultValue={([]).map(s => {
return {
storageUrl: s
}
})}
/>
</Form.Item>}
<Divider />
<Row>
<Col span={14}>
<Form.Item
name="qrcodeImg"
label="二维码照片"
rules={[]}
>
<Uploads
listType='picture-card'
uploadType='image'
maxFilesNum={1}
maxFileSize={5}
isQiniu={true}
fileTypes={["png", "jpg", "jpeg"]}
defaultValue={([]).map(s => {
return {
storageUrl: s
}
})}
/>
</Form.Item>
<Form.Item
name="labels"
label="标签"
rules={[]}
>
<Select
mode="tags"
style={{ width: '80%' }}
placeholder="输入或选择"
onChange={handleChange}
options={labels}
fieldNames={{ label: 'name', value: 'id' }}
/>
</Form.Item>
</Col>
<Col span={10}>
{qrcode
? <Image width={220} src={qrcode} />
: <canvas id="qrcodeCanvas" width="220" height="220" />
}
</Col>
</Row>
</Form>
</Modal>
);
};
function mapStateToProps(state) {
const { auth, labels } = state
return {
user: auth.user,
labels: labels.data || [],
}
}
export default connect(mapStateToProps)(PublicityInfoModal);
function canvasToBlob(canvas) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('无法生成Blob对象'));
}
});
});
}

145
web/client/src/sections/publicityInfoConfig/containers/publicityInfoConfig.js

@ -1,16 +1,147 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Form, Input, Select, Button, message } from 'antd'; import { Form, Input, Select, Button, message, Popconfirm } from 'antd';
import ProTable from '@ant-design/pro-table';
import PublicityInfoModal from '../components/publicityInfoModal';
import moment from 'moment';
import './publicityInfoConfig.less'; import './publicityInfoConfig.less';
const PublicityInfoConfig = (props) => { const PublicityInfoConfig = (props) => {
const { dispatch, actions } = props const { dispatch, actions } = props
const { getPublicityInfo, delPublicityInfo } = actions.publicityInfoConfig
const ref = useRef()
return ( const [dataSource, setDataSource] = useState([]);
<div className='global-main'> const [visible, setVisible] = useState(false);
宣传信息配置 const [curRecord, setCurRecord] = useState();
</div>
) const columns = [
{
title: '宣传标题',
dataIndex: 'name',
key: 'name',
ellipsis: true,
},
{
title: '标签',
dataIndex: 'label',
key: 'label',
ellipsis: true,
search: false,
render: (_, record) => {
return <div>{record.qrcodeLabels?.map(l => l.name)?.join() || '-'}</div>
}
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100,
ellipsis: true,
search: false,
},
{
title: '附件',
dataIndex: 'files',
key: 'files',
search: false,
ellipsis: true,
render: (_, record) => {
return <div>{
record.type === '链接'
? <a href={record.link} target="_blank">{record.link}</a>
: record.qrcodeFiles?.map(f => <div>
<a href={'/_file-server/' + f.fileUrl}>{f.fileName}</a>
</div>)
}</div>
}
},
{
title: '时间',
dataIndex: 'time',
key: 'time',
search: false,
ellipsis: true,
width: 150,
render: (_, record) => {
return <div>{moment(record.time).format('YYYY-MM-DD HH:mm:ss')}</div>
}
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
search: false,
ellipsis: true,
width: 150,
render: (_, record) => {
return <>
{/* <Button type="link" onClick={() => {
setCurRecord(record);
setVisible(true);
}}>编辑</Button> */}
<Popconfirm
title="确定删除吗?"
onConfirm={() => {
dispatch(delPublicityInfo(record.id, record.qrcodeId)).then(() => {
message.success('删除成功');
ref.current.reloadAndRest();
})
}}
onCancel={() => { }}
okText="确定"
cancelText="取消"
>
<Button danger type="link">删除</Button>
</Popconfirm >
</>
}
},
];
return (<>
<ProTable
actionRef={ref}
columns={columns}
dataSource={dataSource || []}
pagination={{ pageSize: 10, size: 'default' }}
options={false}
rowKey="id"
request={async (params = {}) => {
const res = await dispatch(getPublicityInfo({
limit: params?.pageSize,
page: params?.current - 1,
name: params?.name,
}));
setDataSource(res?.payload.data?.rows);
return {
...res,
total: res.payload.data.count ? res.payload.data.count : 0,
};
}}
search={{
defaultCollapsed: false,
optionRender: (searchConfig, formProps, dom) => [
...dom.reverse(),
<Button
key="add"
type='primary'
onClick={() => { setVisible(true) }}
>新增</Button>,
],
}}
onReset={() => { }}
/>
{visible && <PublicityInfoModal
visible={visible}
curRecord={curRecord}
updateList={() => { ref.current.reloadAndRest() }}
onCancel={() => {
setVisible(false)
setCurRecord()
}}
/>}
</>)
} }
function mapStateToProps(state) { function mapStateToProps(state) {

89
web/client/src/sections/qrCode/actions/qrCode.js

@ -3,98 +3,13 @@
import { basicAction } from '@peace/utils' import { basicAction } from '@peace/utils'
import { ApiTable } from '$utils' import { ApiTable } from '$utils'
export function getQrCode (query) {
export function getProjectList (query) {
return (dispatch) => basicAction({
type: 'get',
query,
dispatch,
actionType: 'GET_PROJEECT_LIST',
url: ApiTable.getProjectList,
msg: { error: '获取结构物列表失败', },
});
}
export function postAddProject (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: '删除结构物',
},
});
}
export function addPosition (data, point) {
return (dispatch) => basicAction({
type: 'post',
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' }
});
}
export function delPosition (id) {
return (dispatch) => basicAction({
type: 'del',
dispatch,
actionType: 'DEL_POSITION',
url: ApiTable.delPosition.replace('{id}', id),
msg: {
option: '删除点位',
},
});
}
export function qrCodeShow (query) {
return (dispatch) => basicAction({ return (dispatch) => basicAction({
type: 'get', type: 'get',
query, query,
dispatch, dispatch,
actionType: 'GET_QR_CODE', actionType: 'GET_QR_CODE',
url: ApiTable.qrCodeShow, url: ApiTable.getQrCode,
msg: { error: '获取二维码列表失败', },
});
}
export function q () {
return (dispatch) => basicAction({
type: 'get',
dispatch,
actionType: 'GET_CODE',
url: ApiTable.q,
msg: { error: '获取二维码列表失败', }, msg: { error: '获取二维码列表失败', },
}); });
} }

3
web/client/src/sections/qrCode/containers/index.js

@ -2,5 +2,6 @@
import QrCode from './qrCode' import QrCode from './qrCode'
import ScanCode from './scanCode'
export { QrCode, }; export { QrCode, ScanCode };

18
web/client/src/sections/qrCode/containers/qrCode.js

@ -17,7 +17,7 @@ const QrCode = (props) => {
const projectList = (obj) => { const projectList = (obj) => {
const { projectId, name } = obj const { projectId, name } = obj
dispatch(qrCode.qrCodeShow({ projectId, name })).then(res => { dispatch(qrCode.getQrCode({ name })).then(res => {
if (res.success) { if (res.success) {
setTableList(res.payload.data?.rows) setTableList(res.payload.data?.rows)
} }
@ -39,7 +39,7 @@ const QrCode = (props) => {
let promiseArr = [], nameArr = [] let promiseArr = [], nameArr = []
tableList.forEach(l => { tableList.forEach(l => {
promiseArr.push( promiseArr.push(
window.fetch(`/_file-server/${l.qrCode}`, { window.fetch(`/_file-server/${l.url}`, {
method: 'get', method: 'get',
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
@ -60,7 +60,7 @@ const QrCode = (props) => {
} }
zip.generateAsync({ type: "blob" }).then(blob => { zip.generateAsync({ type: "blob" }).then(blob => {
const url = window.URL.createObjectURL(blob) const url = window.URL.createObjectURL(blob)
downloadFile(url, "点位二维码.zip") downloadFile(url, "二维码.zip")
setDownloading(false) setDownloading(false)
message.success('下载成功') message.success('下载成功')
}); });
@ -87,11 +87,11 @@ const QrCode = (props) => {
}} }}
> >
<Form.Item <Form.Item
label='点位名称' label='名称'
name="name" name="name"
style={{ marginRight: 16, width: 260 }} style={{ marginRight: 16, width: 260 }}
> >
<Input placeholder="请输入点位名称" allowClear /> <Input placeholder="请输入名称" allowClear />
</Form.Item> </Form.Item>
<Form.Item wrapperCol={{}}> <Form.Item wrapperCol={{}}>
<Button htmlType="submit" style={{ marginRight: 16 }}>搜索</Button> <Button htmlType="submit" style={{ marginRight: 16 }}>搜索</Button>
@ -111,22 +111,22 @@ const QrCode = (props) => {
borderRadius: '3px' borderRadius: '3px'
}} }}
> >
<img src={`/_file-server/${v.qrCode}`} style={{ display: 'inline-block', width: 234 }} onError={imgError} /> <img src={`/_file-server/${v.url}`} style={{ display: 'inline-block', width: 234 }} onError={imgError} />
<div style={{ <div style={{
display: 'flex', flexDirection: 'column', padding: '6px 0', display: 'flex', flexDirection: 'column', padding: '6px 0',
width: 220, alignItems: 'center' width: 220, alignItems: 'center'
}}> }}>
<span className='stru-name'>{firmList?.filter(u => u.value == v.projectId)[0]?.label}</span> <span className='stru-name'>{firmList?.filter(u => u.value == v.projectId)[0]?.label}</span>
<span className='point-name'>点位名称{v.name}</span> <span className='point-name'>名称{v.name}</span>
</div> </div>
<div style={{ <div style={{
width: 234, height: 60, display: 'flex', width: 234, height: 60, display: 'flex',
justifyContent: 'center', alignItems: 'center', justifyContent: 'center', alignItems: 'center',
}}> }}>
<Button type="primary" onClick={() => { <Button type="primary" onClick={() => {
const tempArr = v.qrCode.split('/') const tempArr = v.url.split('/')
const filename = tempArr[tempArr.length - 1] const filename = tempArr[tempArr.length - 1]
downloadFile(`/_file-server/${v.qrCode}`, filename) downloadFile(`/_file-server/${v.url}`, filename)
}}>下载二维码</Button> }}>下载二维码</Button>
</div> </div>
</div> </div>

141
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 (<>
<SafeArea position='top' />
<AutoCenter>{qrCode.name}</AutoCenter>
<Divider />
{
qrcodeFiles?.map((f, i) => {
const extensionName = getFileExtension(f.fileUrl)
if (fileType.report.includes(extensionName)) {
return <Card key={i}
title={<Space>
<div className='text---' style={{ maxWidth: '60vw' }}>{f.fileName}</div>
<Tag round color='#2db7f5'>{extensionName}</Tag>
</Space>}
extra={<Button
size='small'
color='primary'
onClick={() => { window.open(`/_file-server/${f.fileUrl}`, '_self') }}
>下载</Button>}
>
<Image
src={f.previewImgUrl ? `/_file-server/${f.previewImgUrl}` : '/assets/images/document.svg'}
style={{ width: '100%', height: '200px' }}
fit='contain'
onClick={() => {
// 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)
}}
/>
</Card>
}
else if (fileType.image.includes(extensionName)) {
return <Card key={i} title={<Space>
<div className='text---' style={{ maxWidth: '70vw' }}>{f.fileName}</div>
<Tag round color='#2db7f5'>{extensionName}</Tag>
</Space>}>
<Image
src={`/_file-server/${f.fileUrl}`}
style={{ width: '100%', height: '200px' }}
fit='contain'
/>
</Card >
}
else if (fileType.video.includes(extensionName)) {
return <Card key={i} title={<Space>
<div className='text---' style={{ maxWidth: '70vw' }}>{f.fileName}</div>
<Tag round color='#2db7f5'>{extensionName}</Tag>
</Space>}>
<video
style={{ width: '100%' }}
src={`/_file-server/${f.fileUrl}`}
poster={f.previewImgUrl ? `/_file-server/${f.previewImgUrl}` : undefined}
controls
/>
</Card>
}
})
}
<Popup
visible={visible}
onMaskClick={() => {
setVisible(false)
}}
position='bottom'
bodyStyle={{ height: '80vh' }}
closeOnMaskClick
>
<iframe
width="100%"
height="100%"
frameBorder='1'
src={link}>
</iframe>
<WaterMark content='FREESUM 飞尚科技' />
</Popup>
<SafeArea position='bottom' />
</>)
}
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() : '';
};

16
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;
}

47
web/client/src/sections/qrCode/routes.js

@ -1,21 +1,32 @@
'use strict'; 'use strict';
import { QrCode, } from './containers'; import { QrCode, ScanCode } from './containers';
export default [{ export default [
type: 'inner', {
route: { type: 'inner',
path: '/qrCode', route: {
key: 'qrCode', path: '/qrCode',
breadcrumb: '二维码管理', key: 'qrCode',
component: QrCode, breadcrumb: '二维码管理',
// 不设置 component 则面包屑禁止跳转 component: QrCode,
// childRoutes: [ // 不设置 component 则面包屑禁止跳转
// { // childRoutes: [
// path: '/qrCode', // {
// key: 'qrCode', // path: '/qrCode',
// component: QrCode, // key: 'qrCode',
// breadcrumb: '二维码管理', // component: QrCode,
// }, // breadcrumb: '二维码管理',
// ] // },
// ]
}
},
{
type: 'outer',
route: {
path: '/scan',
key: 'scan',
component: ScanCode,
breadcrumb: '扫码页面',
}
} }
}]; ];

131
web/client/src/utils/webapi.js

@ -4,8 +4,6 @@ import request from 'superagent';
export const ApiTable = { export const ApiTable = {
login: 'login', login: 'login',
logout: 'logout', logout: 'logout',
validatePhone: 'validate/phone',
getUserSiteList: 'user/site/list',
// 组织管理-用户管理 // 组织管理-用户管理
getDepMessage: 'organization/department', getDepMessage: 'organization/department',
@ -19,131 +17,24 @@ export const ApiTable = {
delUser: 'organization/department/user/{ids}', delUser: 'organization/department/user/{ids}',
resetPwd: '/organization/department/user/resetPwd/{id}', 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', getResource: 'resource',
getUserResource: 'user/resource', getUserResource: 'user/resource',
postUserRes: 'user/resource', postUserRes: 'user/resource',
//安全风险预报 // 标签
getSiteWeekRegiste: 'sites/report', getLabels: 'label',
getRiskReportList: 'risk/report', createLabels: 'label/add',
modifyRiskReport: 'risk/report/{riskId}', delLabels: 'label/{id}',
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}',
//项目状态配置 // 宣传信息配置
editProjectStatus: 'project/status', getPublicityInfo: 'publicityInfo',
//工地平面图 createPublicityInfo: 'publicityInfo/add',
getProjectGraph: 'project/{projectId}/planarGraph', updatePublicityInfo: 'publicityInfo/{id}/modify',
createGraph: 'planarGraph/add', delPublicityInfo: 'publicityInfo/{id}/del?qrcodeId={qrcodeId}',
updateGraph: 'planarGraph/{id}/modify',
deleteGraph: 'project/graph/{id}',
getProjectPoints: 'project/{projectId}/all/points',
getDeployPoints: 'picture/{pictureId}/deploy/points',
setDeployPoints: 'set/picture/{pictureId}/deploy/points',
//设备管理 // 二维码
getDeviceList: 'device', getQrCode: 'qrcode',
addDevice: 'device',
modifyDevice: 'device/{id}',
}; };
export const RouteTable = { export const RouteTable = {

4
web/package.json

@ -72,6 +72,7 @@
"ahooks": "^3.7.4", "ahooks": "^3.7.4",
"ali-oss": "^6.17.1", "ali-oss": "^6.17.1",
"antd": "^4.24.5", "antd": "^4.24.5",
"antd-mobile": "^5.34.0",
"antd-theme-generator": "^1.2.8", "antd-theme-generator": "^1.2.8",
"args": "^5.0.1", "args": "^5.0.1",
"array-move": "^3.0.1", "array-move": "^3.0.1",
@ -94,11 +95,12 @@
"mini-dynamic-antd-theme": "^0.5.3", "mini-dynamic-antd-theme": "^0.5.3",
"moment": "^2.22.0", "moment": "^2.22.0",
"npm": "^7.20.6", "npm": "^7.20.6",
"qr-code-with-logo": "^1.1.0",
"qrcode": "^1.5.1", "qrcode": "^1.5.1",
"qs": "^6.10.1", "qs": "^6.10.1",
"react-color": "^2.19.3",
"react-dnd": "^7", "react-dnd": "^7",
"react-dnd-html5-backend": "^7", "react-dnd-html5-backend": "^7",
"react-color": "^2.19.3",
"react-router-breadcrumbs-hoc": "^4.0.1", "react-router-breadcrumbs-hoc": "^4.0.1",
"react-sortable-hoc": "^2.0.0", "react-sortable-hoc": "^2.0.0",
"shortid": "^2.2.16", "shortid": "^2.2.16",

11
web/webpack.config.js

@ -37,6 +37,7 @@ module.exports = {
module: { module: {
rules: [{ rules: [{
test: /\.css$/, test: /\.css$/,
exclude: path.resolve(__dirname, 'node_modules', 'antd-mobile'),
use: ['style-loader', { use: ['style-loader', {
loader: 'css-loader', loader: 'css-loader',
options: { 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$/, test: /\.less$/,
use: ['style-loader', 'css-loader', { use: ['style-loader', 'css-loader', {

13
web/webpack.config.prod.js

@ -46,6 +46,7 @@ module.exports = {
module: { module: {
rules: [{ rules: [{
test: /\.css$/, test: /\.css$/,
exclude: path.resolve(__dirname, 'node_modules', 'antd-mobile'),
use: ['style-loader', { use: ['style-loader', {
loader: 'css-loader', loader: 'css-loader',
options: { 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$/, test: /\.less$/,
use: ['style-loader', 'css-loader', use: ['style-loader', 'css-loader',
@ -68,7 +79,7 @@ module.exports = {
test: /\.jsx?$/, test: /\.jsx?$/,
use: 'babel-loader', use: 'babel-loader',
include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')], include: [PATHS.app, path.resolve(__dirname, 'node_modules', '@peace')],
},{ }, {
test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/, test: /\.(eot|woff|woff2|svg|ttf)([\?]?.*)$/,
loader: "file-loader" loader: "file-loader"
}] }]

Loading…
Cancel
Save